mirror of https://github.com/atom/atom.git
585 lines
17 KiB
JavaScript
585 lines
17 KiB
JavaScript
/** @babel */
|
|
/** @jsx etch.dom */
|
|
|
|
import _ from 'underscore-plus';
|
|
import { CompositeDisposable } from 'atom';
|
|
import etch from 'etch';
|
|
import fs from 'fs-plus';
|
|
import Grim from 'grim';
|
|
import { marked } from 'marked';
|
|
import path from 'path';
|
|
import { shell } from 'electron';
|
|
|
|
export default class DeprecationCopView {
|
|
constructor({ uri }) {
|
|
this.uri = uri;
|
|
this.subscriptions = new CompositeDisposable();
|
|
this.subscriptions.add(
|
|
Grim.on('updated', () => {
|
|
etch.update(this);
|
|
})
|
|
);
|
|
// TODO: Remove conditional when the new StyleManager deprecation APIs reach stable.
|
|
if (atom.styles.onDidUpdateDeprecations) {
|
|
this.subscriptions.add(
|
|
atom.styles.onDidUpdateDeprecations(() => {
|
|
etch.update(this);
|
|
})
|
|
);
|
|
}
|
|
etch.initialize(this);
|
|
this.subscriptions.add(
|
|
atom.commands.add(this.element, {
|
|
'core:move-up': () => {
|
|
this.scrollUp();
|
|
},
|
|
'core:move-down': () => {
|
|
this.scrollDown();
|
|
},
|
|
'core:page-up': () => {
|
|
this.pageUp();
|
|
},
|
|
'core:page-down': () => {
|
|
this.pageDown();
|
|
},
|
|
'core:move-to-top': () => {
|
|
this.scrollToTop();
|
|
},
|
|
'core:move-to-bottom': () => {
|
|
this.scrollToBottom();
|
|
}
|
|
})
|
|
);
|
|
}
|
|
|
|
serialize() {
|
|
return {
|
|
deserializer: this.constructor.name,
|
|
uri: this.getURI(),
|
|
version: 1
|
|
};
|
|
}
|
|
|
|
destroy() {
|
|
this.subscriptions.dispose();
|
|
return etch.destroy(this);
|
|
}
|
|
|
|
update() {
|
|
return etch.update(this);
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div
|
|
className="deprecation-cop pane-item native-key-bindings"
|
|
tabIndex="-1"
|
|
>
|
|
<div className="panel">
|
|
<div className="padded deprecation-overview">
|
|
<div className="pull-right btn-group">
|
|
<button
|
|
className="btn btn-primary check-for-update"
|
|
onclick={event => {
|
|
event.preventDefault();
|
|
this.checkForUpdates();
|
|
}}
|
|
>
|
|
Check for Updates
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="panel-heading">
|
|
<span>Deprecated calls</span>
|
|
</div>
|
|
<ul className="list-tree has-collapsable-children">
|
|
{this.renderDeprecatedCalls()}
|
|
</ul>
|
|
|
|
<div className="panel-heading">
|
|
<span>Deprecated selectors</span>
|
|
</div>
|
|
<ul className="selectors list-tree has-collapsable-children">
|
|
{this.renderDeprecatedSelectors()}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
renderDeprecatedCalls() {
|
|
const deprecationsByPackageName = this.getDeprecatedCallsByPackageName();
|
|
const packageNames = Object.keys(deprecationsByPackageName);
|
|
if (packageNames.length === 0) {
|
|
return <li className="list-item">No deprecated calls</li>;
|
|
} else {
|
|
return packageNames.sort().map(packageName => (
|
|
<li className="deprecation list-nested-item collapsed">
|
|
<div
|
|
className="deprecation-info list-item"
|
|
onclick={event =>
|
|
event.target.parentElement.classList.toggle('collapsed')
|
|
}
|
|
>
|
|
<span className="text-highlight">{packageName || 'atom core'}</span>
|
|
<span>{` (${_.pluralize(
|
|
deprecationsByPackageName[packageName].length,
|
|
'deprecation'
|
|
)})`}</span>
|
|
</div>
|
|
|
|
<ul className="list">
|
|
{this.renderPackageActionsIfNeeded(packageName)}
|
|
{deprecationsByPackageName[packageName].map(
|
|
({ deprecation, stack }) => (
|
|
<li className="list-item deprecation-detail">
|
|
<span className="text-warning icon icon-alert" />
|
|
<div
|
|
className="list-item deprecation-message"
|
|
innerHTML={marked(deprecation.getMessage())}
|
|
/>
|
|
{this.renderIssueURLIfNeeded(
|
|
packageName,
|
|
deprecation,
|
|
this.buildIssueURL(packageName, deprecation, stack)
|
|
)}
|
|
<div className="stack-trace">
|
|
{stack.map(({ functionName, location }) => (
|
|
<div className="stack-line">
|
|
<span>{functionName}</span>
|
|
<span> - </span>
|
|
<a
|
|
className="stack-line-location"
|
|
href={location}
|
|
onclick={event => {
|
|
event.preventDefault();
|
|
this.openLocation(location);
|
|
}}
|
|
>
|
|
{location}
|
|
</a>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</li>
|
|
)
|
|
)}
|
|
</ul>
|
|
</li>
|
|
));
|
|
}
|
|
}
|
|
|
|
renderDeprecatedSelectors() {
|
|
const deprecationsByPackageName = this.getDeprecatedSelectorsByPackageName();
|
|
const packageNames = Object.keys(deprecationsByPackageName);
|
|
if (packageNames.length === 0) {
|
|
return <li className="list-item">No deprecated selectors</li>;
|
|
} else {
|
|
return packageNames.map(packageName => (
|
|
<li className="deprecation list-nested-item collapsed">
|
|
<div
|
|
className="deprecation-info list-item"
|
|
onclick={event =>
|
|
event.target.parentElement.classList.toggle('collapsed')
|
|
}
|
|
>
|
|
<span className="text-highlight">{packageName}</span>
|
|
</div>
|
|
|
|
<ul className="list">
|
|
{this.renderPackageActionsIfNeeded(packageName)}
|
|
{deprecationsByPackageName[packageName].map(
|
|
({ packagePath, sourcePath, deprecation }) => {
|
|
const relativeSourcePath = path.relative(
|
|
packagePath,
|
|
sourcePath
|
|
);
|
|
const issueTitle = `Deprecated selector in \`${relativeSourcePath}\``;
|
|
const issueBody = `In \`${relativeSourcePath}\`: \n\n${
|
|
deprecation.message
|
|
}`;
|
|
return (
|
|
<li className="list-item source-file">
|
|
<a
|
|
className="source-url"
|
|
href={sourcePath}
|
|
onclick={event => {
|
|
event.preventDefault();
|
|
this.openLocation(sourcePath);
|
|
}}
|
|
>
|
|
{relativeSourcePath}
|
|
</a>
|
|
<ul className="list">
|
|
<li className="list-item deprecation-detail">
|
|
<span className="text-warning icon icon-alert" />
|
|
<div
|
|
className="list-item deprecation-message"
|
|
innerHTML={marked(deprecation.message)}
|
|
/>
|
|
{this.renderSelectorIssueURLIfNeeded(
|
|
packageName,
|
|
issueTitle,
|
|
issueBody
|
|
)}
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
);
|
|
}
|
|
)}
|
|
</ul>
|
|
</li>
|
|
));
|
|
}
|
|
}
|
|
|
|
renderPackageActionsIfNeeded(packageName) {
|
|
if (packageName && atom.packages.getLoadedPackage(packageName)) {
|
|
return (
|
|
<div className="padded">
|
|
<div className="btn-group">
|
|
<button
|
|
className="btn check-for-update"
|
|
onclick={event => {
|
|
event.preventDefault();
|
|
this.checkForUpdates();
|
|
}}
|
|
>
|
|
Check for Update
|
|
</button>
|
|
<button
|
|
className="btn disable-package"
|
|
data-package-name={packageName}
|
|
onclick={event => {
|
|
event.preventDefault();
|
|
this.disablePackage(packageName);
|
|
}}
|
|
>
|
|
Disable Package
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
encodeURI(str) {
|
|
return encodeURI(str)
|
|
.replace(/#/g, '%23')
|
|
.replace(/;/g, '%3B')
|
|
.replace(/%20/g, '+');
|
|
}
|
|
|
|
renderSelectorIssueURLIfNeeded(packageName, issueTitle, issueBody) {
|
|
const repoURL = this.getRepoURL(packageName);
|
|
if (repoURL) {
|
|
const issueURL = `${repoURL}/issues/new?title=${this.encodeURI(
|
|
issueTitle
|
|
)}&body=${this.encodeURI(issueBody)}`;
|
|
return (
|
|
<div className="btn-toolbar">
|
|
<button
|
|
className="btn issue-url"
|
|
data-issue-title={issueTitle}
|
|
data-repo-url={repoURL}
|
|
data-issue-url={issueURL}
|
|
onclick={event => {
|
|
event.preventDefault();
|
|
this.openIssueURL(repoURL, issueURL, issueTitle);
|
|
}}
|
|
>
|
|
Report Issue
|
|
</button>
|
|
</div>
|
|
);
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
renderIssueURLIfNeeded(packageName, deprecation, issueURL) {
|
|
if (packageName && issueURL) {
|
|
const repoURL = this.getRepoURL(packageName);
|
|
const issueTitle = `${deprecation.getOriginName()} is deprecated.`;
|
|
return (
|
|
<div className="btn-toolbar">
|
|
<button
|
|
className="btn issue-url"
|
|
data-issue-title={issueTitle}
|
|
data-repo-url={repoURL}
|
|
data-issue-url={issueURL}
|
|
onclick={event => {
|
|
event.preventDefault();
|
|
this.openIssueURL(repoURL, issueURL, issueTitle);
|
|
}}
|
|
>
|
|
Report Issue
|
|
</button>
|
|
</div>
|
|
);
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
buildIssueURL(packageName, deprecation, stack) {
|
|
const repoURL = this.getRepoURL(packageName);
|
|
if (repoURL) {
|
|
const title = `${deprecation.getOriginName()} is deprecated.`;
|
|
const stacktrace = stack
|
|
.map(({ functionName, location }) => `${functionName} (${location})`)
|
|
.join('\n');
|
|
const body = `${deprecation.getMessage()}\n\`\`\`\n${stacktrace}\n\`\`\``;
|
|
return `${repoURL}/issues/new?title=${encodeURI(title)}&body=${encodeURI(
|
|
body
|
|
)}`;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async openIssueURL(repoURL, issueURL, issueTitle) {
|
|
const issue = await this.findSimilarIssue(repoURL, issueTitle);
|
|
if (issue) {
|
|
shell.openExternal(issue.html_url);
|
|
} else if (process.platform === 'win32') {
|
|
// Windows will not launch URLs greater than ~2000 bytes so we need to shrink it
|
|
shell.openExternal((await this.shortenURL(issueURL)) || issueURL);
|
|
} else {
|
|
shell.openExternal(issueURL);
|
|
}
|
|
}
|
|
|
|
async findSimilarIssue(repoURL, issueTitle) {
|
|
const url = 'https://api.github.com/search/issues';
|
|
const repo = repoURL.replace(/http(s)?:\/\/(\d+\.)?github.com\//gi, '');
|
|
const query = `${issueTitle} repo:${repo}`;
|
|
const response = await window.fetch(
|
|
`${url}?q=${encodeURI(query)}&sort=created`,
|
|
{
|
|
method: 'GET',
|
|
headers: {
|
|
Accept: 'application/vnd.github.v3+json',
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}
|
|
);
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
if (data.items) {
|
|
const issues = {};
|
|
for (const issue of data.items) {
|
|
if (issue.title.includes(issueTitle) && !issues[issue.state]) {
|
|
issues[issue.state] = issue;
|
|
}
|
|
}
|
|
|
|
return issues.open || issues.closed;
|
|
}
|
|
}
|
|
}
|
|
|
|
async shortenURL(url) {
|
|
let encodedUrl = encodeURIComponent(url).substr(0, 5000); // is.gd has 5000 char limit
|
|
let incompletePercentEncoding = encodedUrl.indexOf(
|
|
'%',
|
|
encodedUrl.length - 2
|
|
);
|
|
if (incompletePercentEncoding >= 0) {
|
|
// Handle an incomplete % encoding cut-off
|
|
encodedUrl = encodedUrl.substr(0, incompletePercentEncoding);
|
|
}
|
|
|
|
let result = await fetch('https://is.gd/create.php?format=simple', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: `url=${encodedUrl}`
|
|
});
|
|
|
|
return result.text();
|
|
}
|
|
|
|
getRepoURL(packageName) {
|
|
const loadedPackage = atom.packages.getLoadedPackage(packageName);
|
|
if (
|
|
loadedPackage &&
|
|
loadedPackage.metadata &&
|
|
loadedPackage.metadata.repository
|
|
) {
|
|
const url =
|
|
loadedPackage.metadata.repository.url ||
|
|
loadedPackage.metadata.repository;
|
|
return url.replace(/\.git$/, '');
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
getDeprecatedCallsByPackageName() {
|
|
const deprecatedCalls = Grim.getDeprecations();
|
|
deprecatedCalls.sort((a, b) => b.getCallCount() - a.getCallCount());
|
|
const deprecatedCallsByPackageName = {};
|
|
for (const deprecation of deprecatedCalls) {
|
|
const stacks = deprecation.getStacks();
|
|
stacks.sort((a, b) => b.callCount - a.callCount);
|
|
for (const stack of stacks) {
|
|
let packageName = null;
|
|
if (stack.metadata && stack.metadata.packageName) {
|
|
packageName = stack.metadata.packageName;
|
|
} else {
|
|
packageName = (this.getPackageName(stack) || '').toLowerCase();
|
|
}
|
|
|
|
deprecatedCallsByPackageName[packageName] =
|
|
deprecatedCallsByPackageName[packageName] || [];
|
|
deprecatedCallsByPackageName[packageName].push({ deprecation, stack });
|
|
}
|
|
}
|
|
return deprecatedCallsByPackageName;
|
|
}
|
|
|
|
getDeprecatedSelectorsByPackageName() {
|
|
const deprecatedSelectorsByPackageName = {};
|
|
if (atom.styles.getDeprecations) {
|
|
const deprecatedSelectorsBySourcePath = atom.styles.getDeprecations();
|
|
for (const sourcePath of Object.keys(deprecatedSelectorsBySourcePath)) {
|
|
const deprecation = deprecatedSelectorsBySourcePath[sourcePath];
|
|
const components = sourcePath.split(path.sep);
|
|
const packagesComponentIndex = components.indexOf('packages');
|
|
let packageName = null;
|
|
let packagePath = null;
|
|
if (packagesComponentIndex === -1) {
|
|
packageName = 'Other'; // could be Atom Core or the personal style sheet
|
|
packagePath = '';
|
|
} else {
|
|
packageName = components[packagesComponentIndex + 1];
|
|
packagePath = components
|
|
.slice(0, packagesComponentIndex + 1)
|
|
.join(path.sep);
|
|
}
|
|
|
|
deprecatedSelectorsByPackageName[packageName] =
|
|
deprecatedSelectorsByPackageName[packageName] || [];
|
|
deprecatedSelectorsByPackageName[packageName].push({
|
|
packagePath,
|
|
sourcePath,
|
|
deprecation
|
|
});
|
|
}
|
|
}
|
|
|
|
return deprecatedSelectorsByPackageName;
|
|
}
|
|
|
|
getPackageName(stack) {
|
|
const packagePaths = this.getPackagePathsByPackageName();
|
|
for (const [packageName, packagePath] of packagePaths) {
|
|
if (
|
|
packagePath.includes('.atom/dev/packages') ||
|
|
packagePath.includes('.atom/packages')
|
|
) {
|
|
packagePaths.set(packageName, fs.absolute(packagePath));
|
|
}
|
|
}
|
|
|
|
for (let i = 1; i < stack.length; i++) {
|
|
const { fileName } = stack[i];
|
|
|
|
// Empty when it was run from the dev console
|
|
if (!fileName) {
|
|
return null;
|
|
}
|
|
|
|
// Continue to next stack entry if call is in node_modules
|
|
if (fileName.includes(`${path.sep}node_modules${path.sep}`)) {
|
|
continue;
|
|
}
|
|
|
|
for (const [packageName, packagePath] of packagePaths) {
|
|
const relativePath = path.relative(packagePath, fileName);
|
|
if (!/^\.\./.test(relativePath)) {
|
|
return packageName;
|
|
}
|
|
}
|
|
|
|
if (atom.getUserInitScriptPath() === fileName) {
|
|
return `Your local ${path.basename(fileName)} file`;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
getPackagePathsByPackageName() {
|
|
if (this.packagePathsByPackageName) {
|
|
return this.packagePathsByPackageName;
|
|
} else {
|
|
this.packagePathsByPackageName = new Map();
|
|
for (const pack of atom.packages.getLoadedPackages()) {
|
|
this.packagePathsByPackageName.set(pack.name, pack.path);
|
|
}
|
|
return this.packagePathsByPackageName;
|
|
}
|
|
}
|
|
|
|
checkForUpdates() {
|
|
atom.workspace.open('atom://config/updates');
|
|
}
|
|
|
|
disablePackage(packageName) {
|
|
if (packageName) {
|
|
atom.packages.disablePackage(packageName);
|
|
}
|
|
}
|
|
|
|
openLocation(location) {
|
|
let pathToOpen = location.replace('file://', '');
|
|
if (process.platform === 'win32') {
|
|
pathToOpen = pathToOpen.replace(/^\//, '');
|
|
}
|
|
atom.open({ pathsToOpen: [pathToOpen] });
|
|
}
|
|
|
|
getURI() {
|
|
return this.uri;
|
|
}
|
|
|
|
getTitle() {
|
|
return 'Deprecation Cop';
|
|
}
|
|
|
|
getIconName() {
|
|
return 'alert';
|
|
}
|
|
|
|
scrollUp() {
|
|
this.element.scrollTop -= document.body.offsetHeight / 20;
|
|
}
|
|
|
|
scrollDown() {
|
|
this.element.scrollTop += document.body.offsetHeight / 20;
|
|
}
|
|
|
|
pageUp() {
|
|
this.element.scrollTop -= this.element.offsetHeight;
|
|
}
|
|
|
|
pageDown() {
|
|
this.element.scrollTop += this.element.offsetHeight;
|
|
}
|
|
|
|
scrollToTop() {
|
|
this.element.scrollTop = 0;
|
|
}
|
|
|
|
scrollToBottom() {
|
|
this.element.scrollTop = this.element.scrollHeight;
|
|
}
|
|
}
|