From 692f6c344ea1fb183ca0c1c191644ba3ce5e3536 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 1 Oct 2018 16:12:43 -0700 Subject: [PATCH] :arrow_right: Migrate core package 'deprecation-cop' into ./packages --- package-lock.json | 9 +- package.json | 4 +- packages/README.md | 3 +- packages/deprecation-cop/.coffeelintignore | 1 + packages/deprecation-cop/.gitignore | 3 + packages/deprecation-cop/LICENSE.md | 20 + packages/deprecation-cop/README.md | 6 + packages/deprecation-cop/coffeelint.json | 37 ++ .../deprecation-cop-status-bar-view.coffee | 68 +++ .../lib/deprecation-cop-view.js | 467 ++++++++++++++++++ packages/deprecation-cop/lib/main.js | 39 ++ packages/deprecation-cop/package.json | 31 ++ .../spec/deprecation-cop-spec.coffee | 36 ++ ...eprecation-cop-status-bar-view-spec.coffee | 72 +++ .../spec/deprecation-cop-view-spec.coffee | 93 ++++ .../styles/deprecation-cop.less | 72 +++ 16 files changed, 951 insertions(+), 10 deletions(-) create mode 100644 packages/deprecation-cop/.coffeelintignore create mode 100644 packages/deprecation-cop/.gitignore create mode 100644 packages/deprecation-cop/LICENSE.md create mode 100644 packages/deprecation-cop/README.md create mode 100644 packages/deprecation-cop/coffeelint.json create mode 100644 packages/deprecation-cop/lib/deprecation-cop-status-bar-view.coffee create mode 100644 packages/deprecation-cop/lib/deprecation-cop-view.js create mode 100644 packages/deprecation-cop/lib/main.js create mode 100644 packages/deprecation-cop/package.json create mode 100644 packages/deprecation-cop/spec/deprecation-cop-spec.coffee create mode 100644 packages/deprecation-cop/spec/deprecation-cop-status-bar-view-spec.coffee create mode 100644 packages/deprecation-cop/spec/deprecation-cop-view-spec.coffee create mode 100644 packages/deprecation-cop/styles/deprecation-cop.less diff --git a/package-lock.json b/package-lock.json index f739b8635..7286db1ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1712,8 +1712,7 @@ } }, "deprecation-cop": { - "version": "https://www.atom.io/api/packages/deprecation-cop/versions/0.56.9/tarball", - "integrity": "sha512-dTKNhWcDgK6Y5cR8dwZ507QW15lob+Lp//P71wXoTVidXboDqH13Y1yQ7Av5qASscv7fqp5GcvLhQWF55W5yng==", + "version": "file:packages/deprecation-cop", "requires": { "etch": "0.9.0", "fs-plus": "^3.0.0", @@ -1724,13 +1723,11 @@ "dependencies": { "etch": { "version": "0.9.0", - "resolved": "https://registry.npmjs.org/etch/-/etch-0.9.0.tgz", - "integrity": "sha1-CSJpiPLO4GkL3yCMyyXkFNXfrV8=" + "bundled": true }, "grim": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/grim/-/grim-2.0.2.tgz", - "integrity": "sha1-52CinKe4NDsMH/r2ziDyGkbuiu0=", + "bundled": true, "requires": { "event-kit": "^2.0.0" } diff --git a/package.json b/package.json index c8fcf427a..2f60d17d1 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "command-palette": "https://www.atom.io/api/packages/command-palette/versions/0.43.5/tarball", "dalek": "https://www.atom.io/api/packages/dalek/versions/0.2.2/tarball", "dedent": "^0.7.0", - "deprecation-cop": "https://www.atom.io/api/packages/deprecation-cop/versions/0.56.9/tarball", + "deprecation-cop": "file:packages/deprecation-cop", "dev-live-reload": "file:packages/dev-live-reload", "devtron": "1.3.0", "encoding-selector": "https://www.atom.io/api/packages/encoding-selector/versions/0.23.9/tarball", @@ -196,7 +196,7 @@ "bracket-matcher": "0.89.3", "command-palette": "0.43.5", "dalek": "0.2.2", - "deprecation-cop": "0.56.9", + "deprecation-cop": "file:./packages/deprecation-cop", "dev-live-reload": "file:./packages/dev-live-reload", "encoding-selector": "0.23.9", "exception-reporting": "file:./packages/exception-reporting", diff --git a/packages/README.md b/packages/README.md index ccf1081d6..e4f1925bb 100644 --- a/packages/README.md +++ b/packages/README.md @@ -27,7 +27,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate | **bracket-matcher** | [`atom/bracket-matcher`][bracket-matcher] | | | **command-palette** | [`atom/command-palette`][command-palette] | | | **dalek** | [`atom/dalek`][dalek] | [#17838](https://github.com/atom/atom/issues/17838) | -| **deprecation-cop** | [`atom/deprecation-cop`][deprecation-cop] | [#17839](https://github.com/atom/atom/issues/17839) | +| **deprecation-cop** | [`./deprecation-cop`](./deprecation-cop) | [#17839](https://github.com/atom/atom/issues/17839) | | **dev-live-reload** | [`./dev-live-reload`](dev-live-reload) | [#17840](https://github.com/atom/atom/issues/17840) | | **encoding-selector** | [`atom/encoding-selector`][encoding-selector] | [#17841](https://github.com/atom/atom/issues/17841) | | **exception-reporting** | [`./exception-reporting`](./exception-reporting) | [#17842](https://github.com/atom/atom/issues/17842) | @@ -114,7 +114,6 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate [bracket-matcher]: https://github.com/atom/bracket-matcher [command-palette]: https://github.com/atom/command-palette [dalek]: https://github.com/atom/dalek -[deprecation-cop]: https://github.com/atom/deprecation-cop [encoding-selector]: https://github.com/atom/encoding-selector [find-and-replace]: https://github.com/atom/find-and-replace [fuzzy-finder]: https://github.com/atom/fuzzy-finder diff --git a/packages/deprecation-cop/.coffeelintignore b/packages/deprecation-cop/.coffeelintignore new file mode 100644 index 000000000..1db51fed7 --- /dev/null +++ b/packages/deprecation-cop/.coffeelintignore @@ -0,0 +1 @@ +spec/fixtures diff --git a/packages/deprecation-cop/.gitignore b/packages/deprecation-cop/.gitignore new file mode 100644 index 000000000..ade14b919 --- /dev/null +++ b/packages/deprecation-cop/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +npm-debug.log +node_modules diff --git a/packages/deprecation-cop/LICENSE.md b/packages/deprecation-cop/LICENSE.md new file mode 100644 index 000000000..7ce8c08ee --- /dev/null +++ b/packages/deprecation-cop/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2014 + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/deprecation-cop/README.md b/packages/deprecation-cop/README.md new file mode 100644 index 000000000..749ddba47 --- /dev/null +++ b/packages/deprecation-cop/README.md @@ -0,0 +1,6 @@ +# Deprecation Cop package +[![OS X Build Status](https://travis-ci.org/atom/deprecation-cop.svg?branch=master)](https://travis-ci.org/atom/deprecation-cop) [![Windows Build Status](https://ci.appveyor.com/api/projects/status/0s870q5fj3vwihjx/branch/master?svg=true)](https://ci.appveyor.com/project/Atom/deprecation-cop/branch/master) [![Dependency Status](https://david-dm.org/atom/deprecation-cop.svg)](https://david-dm.org/atom/deprecation-cop) + +Shows a list of deprecated methods calls. Ideally it should show nothing! + +![https://github-images.s3.amazonaws.com/skitch/Deprecation_Cop_-__Users_corey_github_deprecation-cop-20140414-144618.jpg](https://github-images.s3.amazonaws.com/skitch/Deprecation_Cop_-__Users_corey_github_deprecation-cop-20140414-144618.jpg) diff --git a/packages/deprecation-cop/coffeelint.json b/packages/deprecation-cop/coffeelint.json new file mode 100644 index 000000000..a5dd715e3 --- /dev/null +++ b/packages/deprecation-cop/coffeelint.json @@ -0,0 +1,37 @@ +{ + "max_line_length": { + "level": "ignore" + }, + "no_empty_param_list": { + "level": "error" + }, + "arrow_spacing": { + "level": "error" + }, + "no_interpolation_in_single_quotes": { + "level": "error" + }, + "no_debugger": { + "level": "error" + }, + "prefer_english_operator": { + "level": "error" + }, + "colon_assignment_spacing": { + "spacing": { + "left": 0, + "right": 1 + }, + "level": "error" + }, + "braces_spacing": { + "spaces": 0, + "level": "error" + }, + "spacing_after_comma": { + "level": "error" + }, + "no_stand_alone_at": { + "level": "error" + } +} diff --git a/packages/deprecation-cop/lib/deprecation-cop-status-bar-view.coffee b/packages/deprecation-cop/lib/deprecation-cop-status-bar-view.coffee new file mode 100644 index 000000000..889492326 --- /dev/null +++ b/packages/deprecation-cop/lib/deprecation-cop-status-bar-view.coffee @@ -0,0 +1,68 @@ +{CompositeDisposable, Disposable} = require 'atom' +_ = require 'underscore-plus' +Grim = require 'grim' + +module.exports = +class DeprecationCopStatusBarView + lastLength: null + toolTipDisposable: null + + constructor: -> + @subscriptions = new CompositeDisposable + + @element = document.createElement('div') + @element.classList.add('deprecation-cop-status', 'inline-block', 'text-warning') + @element.setAttribute('tabindex', -1) + + @icon = document.createElement('span') + @icon.classList.add('icon', 'icon-alert') + @element.appendChild(@icon) + + @deprecationNumber = document.createElement('span') + @deprecationNumber.classList.add('deprecation-number') + @deprecationNumber.textContent = '0' + @element.appendChild(@deprecationNumber) + + clickHandler = -> + workspaceElement = atom.views.getView(atom.workspace) + atom.commands.dispatch workspaceElement, 'deprecation-cop:view' + @element.addEventListener('click', clickHandler) + @subscriptions.add(new Disposable(=> @element.removeEventListener('click', clickHandler))) + + @update() + + debouncedUpdateDeprecatedSelectorCount = _.debounce(@update, 1000) + + @subscriptions.add Grim.on 'updated', @update + # TODO: Remove conditional when the new StyleManager deprecation APIs reach stable. + if atom.styles.onDidUpdateDeprecations? + @subscriptions.add(atom.styles.onDidUpdateDeprecations(debouncedUpdateDeprecatedSelectorCount)) + + destroy: -> + @subscriptions.dispose() + @element.remove() + + getDeprecatedCallCount: -> + Grim.getDeprecations().map((d) -> d.getStackCount()).reduce(((a, b) -> a + b), 0) + + getDeprecatedStyleSheetsCount: -> + # TODO: Remove conditional when the new StyleManager deprecation APIs reach stable. + if atom.styles.getDeprecations? + Object.keys(atom.styles.getDeprecations()).length + else + 0 + + update: => + length = @getDeprecatedCallCount() + @getDeprecatedStyleSheetsCount() + + return if @lastLength is length + + @lastLength = length + @deprecationNumber.textContent = "#{_.pluralize(length, 'deprecation')}" + @toolTipDisposable?.dispose() + @toolTipDisposable = atom.tooltips.add @element, title: "#{_.pluralize(length, 'call')} to deprecated methods" + + if length is 0 + @element.style.display = 'none' + else + @element.style.display = '' diff --git a/packages/deprecation-cop/lib/deprecation-cop-view.js b/packages/deprecation-cop/lib/deprecation-cop-view.js new file mode 100644 index 000000000..01d8ad736 --- /dev/null +++ b/packages/deprecation-cop/lib/deprecation-cop-view.js @@ -0,0 +1,467 @@ +/** @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 'shell' + +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 ( +
+
+
+
+ +
+
+ +
Deprecated calls
+
    + {this.renderDeprecatedCalls()} +
+ +
Deprecated selectors
+
    + {this.renderDeprecatedSelectors()} +
+
+
+ ) + } + + renderDeprecatedCalls () { + const deprecationsByPackageName = this.getDeprecatedCallsByPackageName() + const packageNames = Object.keys(deprecationsByPackageName) + if (packageNames.length === 0) { + return
  • No deprecated calls
  • + } else { + return packageNames.sort().map((packageName) => ( +
  • +
    event.target.parentElement.classList.toggle('collapsed')}> + {packageName || 'atom core'} + {` (${_.pluralize(deprecationsByPackageName[packageName].length, 'deprecation')})`} +
    + +
      + {this.renderPackageActionsIfNeeded(packageName)} + {deprecationsByPackageName[packageName].map(({deprecation, stack}) => ( +
    • + +
      + {this.renderIssueURLIfNeeded(packageName, deprecation, this.buildIssueURL(packageName, deprecation, stack))} +
      + {stack.map(({functionName, location}) => ( + + ))} +
      +
    • + ))} +
    +
  • + )) + } + } + + renderDeprecatedSelectors () { + const deprecationsByPackageName = this.getDeprecatedSelectorsByPackageName() + const packageNames = Object.keys(deprecationsByPackageName) + if (packageNames.length === 0) { + return ( +
  • No deprecated selectors
  • + ) + } else { + return packageNames.map((packageName) => ( +
  • +
    event.target.parentElement.classList.toggle('collapsed')}> + {packageName} +
    + +
      + {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 ( +
    • + { + event.preventDefault() + this.openLocation(sourcePath) + }}>{relativeSourcePath} +
        +
      • + +
        + {this.renderSelectorIssueURLIfNeeded(packageName, issueTitle, issueBody)} +
      • +
      +
    • + ) + })} +
    +
  • + )) + } + } + + renderPackageActionsIfNeeded (packageName) { + if (packageName && atom.packages.getLoadedPackage(packageName)) { + return ( +
    +
    + + +
    +
    + ) + } 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 ( +
    + +
    + ) + } else { + return '' + } + } + + renderIssueURLIfNeeded (packageName, deprecation, issueURL) { + if (packageName && issueURL) { + const repoURL = this.getRepoURL(packageName) + const issueTitle = `${deprecation.getOriginName()} is deprecated.` + return ( +
    + +
    + ) + } 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 + } +} diff --git a/packages/deprecation-cop/lib/main.js b/packages/deprecation-cop/lib/main.js new file mode 100644 index 000000000..12da158b5 --- /dev/null +++ b/packages/deprecation-cop/lib/main.js @@ -0,0 +1,39 @@ +const {Disposable, CompositeDisposable} = require('atom') +const DeprecationCopView = require('./deprecation-cop-view') +const DeprecationCopStatusBarView = require('./deprecation-cop-status-bar-view') +const ViewURI = 'atom://deprecation-cop' + +class DeprecationCopPackage { + activate () { + this.disposables = new CompositeDisposable() + this.disposables.add(atom.workspace.addOpener((uri) => { + if (uri === ViewURI) { + return this.deserializeDeprecationCopView({uri}) + } + })) + this.disposables.add(atom.commands.add('atom-workspace', 'deprecation-cop:view', () => { + atom.workspace.open(ViewURI) + })) + } + + deactivate () { + this.disposables.dispose() + const pane = atom.workspace.paneForURI(ViewURI) + if (pane) { + pane.destroyItem(pane.itemForURI(ViewURI)) + } + } + + deserializeDeprecationCopView (state) { + return new DeprecationCopView(state) + } + + consumeStatusBar (statusBar) { + const statusBarView = new DeprecationCopStatusBarView() + const statusBarTile = statusBar.addRightTile({item: statusBarView, priority: 150}) + this.disposables.add(new Disposable(() => { statusBarView.destroy() })) + this.disposables.add(new Disposable(() => { statusBarTile.destroy() })) + } +} + +module.exports = new DeprecationCopPackage() diff --git a/packages/deprecation-cop/package.json b/packages/deprecation-cop/package.json new file mode 100644 index 000000000..30d95d54d --- /dev/null +++ b/packages/deprecation-cop/package.json @@ -0,0 +1,31 @@ +{ + "name": "deprecation-cop", + "main": "./lib/main", + "version": "0.56.9", + "description": "Shows a list of deprecated calls", + "repository": "https://github.com/atom/atom", + "license": "MIT", + "engines": { + "atom": ">0.50.0" + }, + "dependencies": { + "etch": "0.9.0", + "fs-plus": "^3.0.0", + "grim": "^2.0.1", + "marked": "^0.3.6", + "underscore-plus": "^1.0.0" + }, + "consumedServices": { + "status-bar": { + "versions": { + "^1.0.0": "consumeStatusBar" + } + } + }, + "deserializers": { + "DeprecationCopView": "deserializeDeprecationCopView" + }, + "devDependencies": { + "coffeelint": "^1.9.7" + } +} diff --git a/packages/deprecation-cop/spec/deprecation-cop-spec.coffee b/packages/deprecation-cop/spec/deprecation-cop-spec.coffee new file mode 100644 index 000000000..bc0e9bf0c --- /dev/null +++ b/packages/deprecation-cop/spec/deprecation-cop-spec.coffee @@ -0,0 +1,36 @@ +DeprecationCopView = require '../lib/deprecation-cop-view' + +describe "DeprecationCop", -> + [activationPromise, workspaceElement] = [] + + beforeEach -> + workspaceElement = atom.views.getView(atom.workspace) + activationPromise = atom.packages.activatePackage('deprecation-cop') + expect(atom.workspace.getActivePane().getActiveItem()).not.toExist() + + describe "when the deprecation-cop:view event is triggered", -> + it "displays the deprecation cop pane", -> + atom.commands.dispatch workspaceElement, 'deprecation-cop:view' + + waitsForPromise -> + activationPromise + + deprecationCopView = null + waitsFor -> + deprecationCopView = atom.workspace.getActivePane().getActiveItem() + + runs -> + expect(deprecationCopView instanceof DeprecationCopView).toBeTruthy() + + describe "deactivating the package", -> + it "removes the deprecation cop pane item", -> + atom.commands.dispatch workspaceElement, 'deprecation-cop:view' + + waitsForPromise -> + activationPromise + + waitsForPromise -> + Promise.resolve(atom.packages.deactivatePackage('deprecation-cop')) # Wrapped for Promise & non-Promise deactivate + + runs -> + expect(atom.workspace.getActivePane().getActiveItem()).not.toExist() diff --git a/packages/deprecation-cop/spec/deprecation-cop-status-bar-view-spec.coffee b/packages/deprecation-cop/spec/deprecation-cop-status-bar-view-spec.coffee new file mode 100644 index 000000000..ba93692b2 --- /dev/null +++ b/packages/deprecation-cop/spec/deprecation-cop-status-bar-view-spec.coffee @@ -0,0 +1,72 @@ +path = require 'path' +Grim = require 'grim' +DeprecationCopView = require '../lib/deprecation-cop-view' +_ = require 'underscore-plus' + +describe "DeprecationCopStatusBarView", -> + [deprecatedMethod, statusBarView, workspaceElement] = [] + + beforeEach -> + # jasmine.Clock.useMock() cannot mock _.debounce + # http://stackoverflow.com/questions/13707047/spec-for-async-functions-using-jasmine + spyOn(_, 'debounce').andCallFake (func) -> + -> func.apply(this, arguments) + + jasmine.snapshotDeprecations() + + workspaceElement = atom.views.getView(atom.workspace) + jasmine.attachToDOM(workspaceElement) + waitsForPromise -> atom.packages.activatePackage('status-bar') + waitsForPromise -> atom.packages.activatePackage('deprecation-cop') + + waitsFor -> + statusBarView = workspaceElement.querySelector('.deprecation-cop-status') + + afterEach -> + jasmine.restoreDeprecationsSnapshot() + + it "adds the status bar view when activated", -> + expect(statusBarView).toExist() + expect(statusBarView.textContent).toBe '0 deprecations' + expect(statusBarView).not.toShow() + + it "increments when there are deprecated methods", -> + deprecatedMethod = -> Grim.deprecate("This isn't used") + anotherDeprecatedMethod = -> Grim.deprecate("This either") + expect(statusBarView.style.display).toBe 'none' + expect(statusBarView.offsetHeight).toBe(0) + + deprecatedMethod() + expect(statusBarView.textContent).toBe '1 deprecation' + expect(statusBarView.offsetHeight).toBeGreaterThan(0) + + deprecatedMethod() + expect(statusBarView.textContent).toBe '2 deprecations' + expect(statusBarView.offsetHeight).toBeGreaterThan(0) + + anotherDeprecatedMethod() + expect(statusBarView.textContent).toBe '3 deprecations' + expect(statusBarView.offsetHeight).toBeGreaterThan(0) + + # TODO: Remove conditional when the new StyleManager deprecation APIs reach stable. + if atom.styles.getDeprecations? + it "increments when there are deprecated selectors", -> + atom.styles.addStyleSheet(""" + atom-text-editor::shadow { color: red; } + """, sourcePath: 'file-1') + expect(statusBarView.textContent).toBe '1 deprecation' + expect(statusBarView).toBeVisible() + atom.styles.addStyleSheet(""" + atom-text-editor::shadow { color: blue; } + """, sourcePath: 'file-2') + expect(statusBarView.textContent).toBe '2 deprecations' + expect(statusBarView).toBeVisible() + + it 'opens deprecation cop tab when clicked', -> + expect(atom.workspace.getActivePane().getActiveItem()).not.toExist() + + waitsFor (done) -> + atom.workspace.onDidOpen ({item}) -> + expect(item instanceof DeprecationCopView).toBe true + done() + statusBarView.click() diff --git a/packages/deprecation-cop/spec/deprecation-cop-view-spec.coffee b/packages/deprecation-cop/spec/deprecation-cop-view-spec.coffee new file mode 100644 index 000000000..cae7fe92b --- /dev/null +++ b/packages/deprecation-cop/spec/deprecation-cop-view-spec.coffee @@ -0,0 +1,93 @@ +Grim = require 'grim' +path = require 'path' +_ = require 'underscore-plus' +etch = require 'etch' + +describe "DeprecationCopView", -> + [deprecationCopView, workspaceElement] = [] + + beforeEach -> + spyOn(_, 'debounce').andCallFake (func) -> + -> func.apply(this, arguments) + + workspaceElement = atom.views.getView(atom.workspace) + jasmine.attachToDOM(workspaceElement) + + jasmine.snapshotDeprecations() + Grim.clearDeprecations() + deprecatedMethod = -> Grim.deprecate("A test deprecation. This isn't used") + deprecatedMethod() + + spyOn(Grim, 'deprecate') # Don't fail tests if when using deprecated APIs in deprecation cop's activation + activationPromise = atom.packages.activatePackage('deprecation-cop') + + atom.commands.dispatch workspaceElement, 'deprecation-cop:view' + + waitsForPromise -> + activationPromise + + waitsFor -> deprecationCopView = atom.workspace.getActivePane().getActiveItem() + + runs -> + jasmine.unspy(Grim, 'deprecate') + + afterEach -> + jasmine.restoreDeprecationsSnapshot() + + it "displays deprecated methods", -> + expect(deprecationCopView.element.textContent).toMatch /Deprecated calls/ + expect(deprecationCopView.element.textContent).toMatch /This isn't used/ + + # TODO: Remove conditional when the new StyleManager deprecation APIs reach stable. + if atom.styles.getDeprecations? + it "displays deprecated selectors", -> + atom.styles.addStyleSheet("atom-text-editor::shadow { color: red }", sourcePath: path.join('some-dir', 'packages', 'package-1', 'file-1.css')) + atom.styles.addStyleSheet("atom-text-editor::shadow { color: yellow }", context: 'atom-text-editor', sourcePath: path.join('some-dir', 'packages', 'package-1', 'file-2.css')) + atom.styles.addStyleSheet('atom-text-editor::shadow { color: blue }', sourcePath: path.join('another-dir', 'packages', 'package-2', 'file-3.css')) + atom.styles.addStyleSheet('atom-text-editor::shadow { color: gray }', sourcePath: path.join('another-dir', 'node_modules', 'package-3', 'file-4.css')) + + promise = etch.getScheduler().getNextUpdatePromise() + waitsForPromise -> promise + + runs -> + packageItems = deprecationCopView.element.querySelectorAll("ul.selectors > li") + expect(packageItems.length).toBe(3) + expect(packageItems[0].textContent).toMatch /package-1/ + expect(packageItems[1].textContent).toMatch /package-2/ + expect(packageItems[2].textContent).toMatch /Other/ + + packageDeprecationItems = packageItems[0].querySelectorAll("li.source-file") + expect(packageDeprecationItems.length).toBe(2) + expect(packageDeprecationItems[0].textContent).toMatch /atom-text-editor/ + expect(packageDeprecationItems[0].querySelector("a").href).toMatch('some-dir/packages/package-1/file-1.css') + expect(packageDeprecationItems[1].textContent).toMatch /:host/ + expect(packageDeprecationItems[1].querySelector("a").href).toMatch('some-dir/packages/package-1/file-2.css') + + it 'skips stack entries which go through node_modules/ files when determining package name', -> + stack = [ + { + "functionName": "function0" + "location": path.normalize "/Users/user/.atom/packages/package1/node_modules/atom-space-pen-viewslib/space-pen.js:55:66" + "fileName": path.normalize "/Users/user/.atom/packages/package1/node_modules/atom-space-pen-views/lib/space-pen.js" + } + { + "functionName": "function1" + "location": path.normalize "/Users/user/.atom/packages/package1/node_modules/atom-space-pen-viewslib/space-pen.js:15:16" + "fileName": path.normalize "/Users/user/.atom/packages/package1/node_modules/atom-space-pen-views/lib/space-pen.js" + } + { + "functionName": "function2" + "location": path.normalize "/Users/user/.atom/packages/package2/lib/module.js:13:14" + "fileName": path.normalize "/Users/user/.atom/packages/package2/lib/module.js" + } + ] + + packagePathsByPackageName = new Map([ + ['package1', path.normalize("/Users/user/.atom/packages/package1")], + ['package2', path.normalize("/Users/user/.atom/packages/package2")] + ]) + + spyOn(deprecationCopView, 'getPackagePathsByPackageName').andReturn(packagePathsByPackageName) + + packageName = deprecationCopView.getPackageName(stack) + expect(packageName).toBe("package2") diff --git a/packages/deprecation-cop/styles/deprecation-cop.less b/packages/deprecation-cop/styles/deprecation-cop.less new file mode 100644 index 000000000..d3f4e929e --- /dev/null +++ b/packages/deprecation-cop/styles/deprecation-cop.less @@ -0,0 +1,72 @@ +// The ui-variables file is provided by base themes provided by Atom. +// +// See https://github.com/atom/atom-dark-ui/blob/master/stylesheets/ui-variables.less +// for a full listing of what's available. +@import "ui-variables"; + +.deprecation-cop-status { + .icon.icon-alert:before { + // It's a really big icon... + width: 17px; + } +} +.deprecation-cop { + overflow: auto; + -webkit-flex: 1; + background-color: @app-background-color !important; + + .deprecation-overview { + &:after { + content: ''; + clear: both; + display: block; + } + } + + .deprecation-info { + padding: 0 @component-padding; + } + + .deprecation-info:hover { + background-color: @background-color-selected !important; + } + + .deprecation-detail.list-item { + white-space: normal; + clear: both; + + .deprecation-message { + padding: 5px 0 5px 28px; + line-height: 1.4; + font-size: @font-size; + + p:last-child { + margin-bottom: 0; + } + } + + .icon-alert { + margin-top: 5px; + float: left; + } + } + + .collapsed > ul { + display: none; + } + + .list { + list-style-type: none; + padding: 0; + } + + .stack-trace { + background-color: @tool-panel-background-color; + padding: @component-padding; + margin-bottom: @component-padding; + } + + a { + color: @text-color-highlight; + } +}