diff --git a/frontend/.eslintrc.yml b/frontend/.eslintrc.yml new file mode 100644 index 000000000..c0774ea6b --- /dev/null +++ b/frontend/.eslintrc.yml @@ -0,0 +1,19 @@ +env: + browser: true + es6: true +plugins: + - prettier + - mozilla +extends: + - standard + - prettier + - plugin:mozilla/recommended +globals: + Atomics: readonly + SharedArrayBuffer: readonly +parserOptions: + ecmaVersion: 2018 + sourceType: module +rules: + max-len: off + prettier/prettier: "error" diff --git a/frontend/package.json b/frontend/package.json index 2acaa8853..3d1ee8e65 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,9 +27,20 @@ "babel-polyfill": "^6.26.0", "clean-webpack-plugin": "^3.0.0", "css-loader": "^3.0.0", + "eslint": "^6.2.2", + "eslint-config-prettier": "^6.2.0", + "eslint-config-standard": "^14.1.0", + "eslint-loader": "^3.0.0", + "eslint-plugin-fetch-options": "0.0.5", + "eslint-plugin-html": "^6.0.0", + "eslint-plugin-mozilla": "^2.1.0", + "eslint-plugin-no-unsanitized": "^3.0.2", + "eslint-plugin-prettier": "^3.1.0", "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^0.8.0", "optimize-css-assets-webpack-plugin": "^5.0.3", + "prettier": "1.18.2", + "standard": "^14.1.0", "terser-webpack-plugin": "^1.3.0", "webpack": "^4.35.3", "webpack-cli": "^3.3.6", diff --git a/frontend/src/common.js b/frontend/src/common.js index 4578955ac..152c56284 100644 --- a/frontend/src/common.js +++ b/frontend/src/common.js @@ -1,29 +1,31 @@ -import Mustache from 'mustache'; -import { buildRoute, readRoute, updateRoute } from './route.js'; -import {ZERO_COVERAGE_FILTERS} from './zero_coverage_report.js'; +import Mustache from "mustache"; +import { buildRoute, readRoute, updateRoute } from "./route.js"; +import { ZERO_COVERAGE_FILTERS } from "./zero_coverage_report.js"; -export const REV_LATEST = 'latest'; +export const REV_LATEST = "latest"; function domContentLoaded() { - return new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve)); + return new Promise(resolve => + document.addEventListener("DOMContentLoaded", resolve) + ); } export const DOM_READY = domContentLoaded(); export async function main(load, display) { // Load initial data before DOM is available - let data = await load(); + const data = await load(); // Wait for DOM to be ready before displaying await DOM_READY; await display(data); - monitor_options(); + monitorOptions(); // Full workflow, loading then displaying data // used for following updates - let full = async function() { - let data = await load(); + const full = async function() { + const data = await load(); await display(data); - monitor_options(); + monitorOptions(); }; // React to url changes @@ -34,19 +36,20 @@ export async function main(load, display) { const COVERAGE_BACKEND_HOST = process.env.BACKEND_URL; -function cache_get(cache, key) { +function cacheGet(cache, key) { if (key in cache) { return cache[key].val; } + return null; } -function cache_set(cache, key, value) { - let now = new Date().getTime() / 1000; +function cacheSet(cache, key, value) { + const now = new Date().getTime() / 1000; // If the cache got too big, remove all elements that were added more // than 15 minutes ago. if (Object.keys(cache).length > 100) { - for (let key in cache) { + for (const key in cache) { if (cache[key].time < now - 15 * 60) { delete cache[key]; } @@ -54,15 +57,15 @@ function cache_set(cache, key, value) { } cache[key] = { - 'val': value, - 'time': now, + val: value, + time: now }; } -let path_coverage_cache = {}; -export async function get_path_coverage(path, changeset, platform, suite) { - let cache_key = `${changeset}_${path}_${platform}_${suite}`; - let data = cache_get(path_coverage_cache, cache_key); +const pathCoverageCache = {}; +export async function getPathCoverage(path, changeset, platform, suite) { + const cacheKey = `${changeset}_${path}_${platform}_${suite}`; + let data = cacheGet(pathCoverageCache, cacheKey); if (data) { return data; } @@ -71,54 +74,56 @@ export async function get_path_coverage(path, changeset, platform, suite) { if (changeset && changeset !== REV_LATEST) { params += `&changeset=${changeset}`; } - if (platform && platform !== 'all') { + if (platform && platform !== "all") { params += `&platform=${platform}`; } - if (suite && suite !== 'all') { + if (suite && suite !== "all") { params += `&suite=${suite}`; } - let response = await fetch(`${COVERAGE_BACKEND_HOST}/v2/path?${params}`).catch(alert); + const response = await fetch( + `${COVERAGE_BACKEND_HOST}/v2/path?${params}` + ).catch(alert); if (response.status !== 200) { - throw new Error(response.status + ' - ' + response.statusText); + throw new Error(response.status + " - " + response.statusText); } data = await response.json(); - cache_set(path_coverage_cache, cache_key, data); + cacheSet(pathCoverageCache, cacheKey, data); return data; } -let history_cache = {}; -export async function get_history(path, platform, suite) { +const historyCache = {}; +export async function getHistory(path, platform, suite) { // Backend needs path without trailing / - if (path && path.endsWith('/')) { - path = path.substring(0, path.length-1); + if (path && path.endsWith("/")) { + path = path.substring(0, path.length - 1); } - let cache_key = `${path}_${platform}_${suite}`; - let data = cache_get(history_cache, cache_key); + const cacheKey = `${path}_${platform}_${suite}`; + let data = cacheGet(historyCache, cacheKey); if (data) { return data; } let params = `path=${path}`; - if (platform && platform !== 'all') { + if (platform && platform !== "all") { params += `&platform=${platform}`; } - if (suite && suite !== 'all') { + if (suite && suite !== "all") { params += `&suite=${suite}`; } - let response = await fetch(`${COVERAGE_BACKEND_HOST}/v2/history?${params}`); + const response = await fetch(`${COVERAGE_BACKEND_HOST}/v2/history?${params}`); data = await response.json(); - cache_set(history_cache, cache_key, data); + cacheSet(historyCache, cacheKey, data); // Check data has coverage values // These values are missing when going above 2 levels right now - let coverage = data.filter(point => { + const coverage = data.filter(point => { return point.coverage !== null; }); - if (coverage.length === 0 ) { + if (coverage.length === 0) { console.warn(`No history data for ${path}`); return null; } @@ -126,110 +131,111 @@ export async function get_history(path, platform, suite) { return data; } -let zero_coverage_cache = {}; -export async function get_zero_coverage_data() { - let data = cache_get(zero_coverage_cache, ''); +const zeroCoverageCache = {}; +export async function getZeroCoverageData() { + let data = cacheGet(zeroCoverageCache, ""); if (data) { return data; } - let response = await fetch('https://index.taskcluster.net/v1/task/project.releng.services.project.production.code_coverage_bot.latest/artifacts/public/zero_coverage_report.json'); + const response = await fetch( + "https://index.taskcluster.net/v1/task/project.releng.services.project.production.code_coverage_bot.latest/artifacts/public/zero_coverage_report.json" + ); data = await response.json(); - cache_set(zero_coverage_cache, '', data); + cacheSet(zeroCoverageCache, "", data); return data; } - -let filters_cache = {}; -export async function get_filters() { - let data = cache_get(filters_cache, ''); +const filtersCache = {}; +export async function getFilters() { + let data = cacheGet(filtersCache, ""); if (data) { return data; } - let response = await fetch(`${COVERAGE_BACKEND_HOST}/v2/filters`); + const response = await fetch(`${COVERAGE_BACKEND_HOST}/v2/filters`); data = await response.json(); - cache_set(filters_cache, '', data); + cacheSet(filtersCache, "", data); return data; } - // Option handling. -export function is_enabled(opt) { - let route = readRoute(); - let value = 'off'; +export function isEnabled(opt) { + const route = readRoute(); + let value = "off"; if (route[opt]) { value = route[opt]; } else if (ZERO_COVERAGE_FILTERS[opt]) { value = ZERO_COVERAGE_FILTERS[opt].default_value; } - return value === 'on'; + return value === "on"; } -function monitor_options() { +function monitorOptions() { // Monitor input & select changes - let fields = document.querySelectorAll('input, select'); - for(let field of fields) { - if (field.type == 'text') { + const fields = document.querySelectorAll("input, select"); + for (const field of fields) { + if (field.type === "text") { // React on enter - field.onkeydown = async (evt) => { - if(evt.keyCode === 13) { - let params = {}; + field.onkeydown = async evt => { + if (evt.keyCode === 13) { + const params = {}; params[evt.target.name] = evt.target.value; updateRoute(params); } - } + }; } else { // React on change - field.onchange = async (evt) => { + field.onchange = async evt => { let value = evt.target.value; - if (evt.target.type == 'checkbox') { - value = evt.target.checked ? 'on' : 'off'; + if (evt.target.type === "checkbox") { + value = evt.target.checked ? "on" : "off"; } - let params = {}; + const params = {}; params[evt.target.name] = value; updateRoute(params); - } + }; } } } // hgmo. -export async function get_source(file) { - let response = await fetch(`https://hg.mozilla.org/mozilla-central/raw-file/tip/${file}`); - return await response.text(); +export async function getSource(file) { + const response = await fetch( + `https://hg.mozilla.org/mozilla-central/raw-file/tip/${file}` + ); + return response.text(); } - // Filtering. -let get_third_party_paths = function() { +const getThirdPartyPaths = (function() { let paths = null; return async function() { if (!paths) { - let response = await get_source('tools/rewriting/ThirdPartyPaths.txt'); - paths = response.split('\n').filter(path => path != ''); + const response = await getSource("tools/rewriting/ThirdPartyPaths.txt"); + paths = response.split("\n").filter(path => path !== ""); } return paths; }; -}(); +})(); -export async function filter_third_party(files) { - if (is_enabled('third_party')) { +export async function filterThirdParty(files) { + if (isEnabled("third_party")) { return files; } - let paths = await get_third_party_paths(); + const paths = await getThirdPartyPaths(); return files.filter(file => { - for (let path of paths) { + for (const path of paths) { if (file.path.startsWith(path)) { return false; } @@ -239,125 +245,144 @@ export async function filter_third_party(files) { }); } -export function filter_languages(files) { - let cpp = is_enabled('cpp'); - let cpp_extensions = ['c', 'cpp', 'cxx', 'cc', 'h', 'hh', 'hxx', 'hpp', 'inl', 'inc']; - let js = is_enabled('js'); - let js_extensions = ['js', 'jsm', 'xml', 'xul', 'xhtml', 'html']; - let java = is_enabled('java'); - let java_extensions = ['java']; - let rust = is_enabled('rust'); - let rust_extensions = ['rs']; +export function filterLanguages(files) { + const cpp = isEnabled("cpp"); + const cppExtensions = [ + "c", + "cpp", + "cxx", + "cc", + "h", + "hh", + "hxx", + "hpp", + "inl", + "inc" + ]; + const js = isEnabled("js"); + const jsExtensions = ["js", "jsm", "xml", "xul", "xhtml", "html"]; + const java = isEnabled("java"); + const javaExtensions = ["java"]; + const rust = isEnabled("rust"); + const rustExtensions = ["rs"]; return files.filter(file => { - if (file.type == "directory") { + if (file.type === "directory") { return true; - } else if (cpp_extensions.find(ext => file.path.endsWith('.' + ext))) { + } else if (cppExtensions.find(ext => file.path.endsWith("." + ext))) { return cpp; - } else if (js_extensions.find(ext => file.path.endsWith('.' + ext))) { + } else if (jsExtensions.find(ext => file.path.endsWith("." + ext))) { return js; - } else if (rust_extensions.find(ext => file.path.endsWith('.' + ext))) { + } else if (rustExtensions.find(ext => file.path.endsWith("." + ext))) { return rust; - } else if (java_extensions.find(ext => file.path.endsWith('.' + ext))) { + } else if (javaExtensions.find(ext => file.path.endsWith("." + ext))) { return java; - } else { - console.warn('Unknown language for ' + file.path); - return false; } + console.warn("Unknown language for " + file.path); + return false; }); } -export function filter_headers(files) { - if (is_enabled('headers')) { +export function filterHeaders(files) { + if (isEnabled("headers")) { return files; } - return files.filter(file => !file.path.endsWith('.h')); + return files.filter(file => !file.path.endsWith(".h")); } -export function filter_completely_uncovered(files) { - if (!is_enabled('completely_uncovered')) { +export function filterCompletelyUncovered(files) { + if (!isEnabled("completely_uncovered")) { return files; } return files.filter(file => file.uncovered); } -export function filter_last_push_date(files) { - let elem = document.getElementById('last_push'); - let upper_limit = new Date(); - let lower_limit = new Date(); - - if (elem.value == 'one_year') { - lower_limit.setFullYear(upper_limit.getFullYear() - 1); - } else if (elem.value == 'two_years') { - upper_limit.setFullYear(upper_limit.getFullYear() - 1); - lower_limit.setFullYear(lower_limit.getFullYear() - 2); - } else if (elem.value == 'older_than_two_years') { - upper_limit.setFullYear(upper_limit.getFullYear() - 2); - lower_limit = new Date('1970-01-01T00:00:00Z'); +export function filterLastPushDate(files) { + const elem = document.getElementById("last_push"); + const upperLimit = new Date(); + let lowerLimit = new Date(); + + if (elem.value === "one_year") { + lowerLimit.setFullYear(upperLimit.getFullYear() - 1); + } else if (elem.value === "two_years") { + upperLimit.setFullYear(upperLimit.getFullYear() - 1); + lowerLimit.setFullYear(lowerLimit.getFullYear() - 2); + } else if (elem.value === "older_than_two_years") { + upperLimit.setFullYear(upperLimit.getFullYear() - 2); + lowerLimit = new Date("1970-01-01T00:00:00Z"); } else { return files; } return files.filter(file => { - let last_push_date = new Date(file.last_push_date); - if (last_push_date.getTime() <= upper_limit.getTime() - && last_push_date.getTime() >= lower_limit.getTime()) { + const lastPushDate = new Date(file.lastPushDate); + if ( + lastPushDate.getTime() <= upperLimit.getTime() && + lastPushDate.getTime() >= lowerLimit.getTime() + ) { return true; - } else { - return false; } + return false; }); } // Build the urls for a breadcrumb Navbar from a path -export function build_navbar(path, revision) { - if (path.endsWith('/')) { - path = path.substring(0, path.length-1); +export function buildNavbar(path, revision) { + if (path.endsWith("/")) { + path = path.substring(0, path.length - 1); } - let base = ''; - let links = [ + let base = ""; + const links = [ { - 'name': 'mozilla-central', - 'route': buildRoute({path: '', revision}) + name: "mozilla-central", + route: buildRoute({ path: "", revision }) } ]; - return links.concat(path.split('/').map(file => { - base += (base ? '/' : '') + file; - return { - 'name': file, - 'route': buildRoute({path: base, revision}) - }; - })); + return links.concat( + path.split("/").map(file => { + base += (base ? "/" : "") + file; + return { + name: file, + route: buildRoute({ path: base, revision }) + }; + }) + ); } // Display helpers function canDisplay() { - return document.readyState == 'complete'; + return document.readyState === "complete"; } export function message(cssClass, message) { - if(!canDisplay()) return; + if (!canDisplay()) { + return; + } - let box = document.getElementById('message'); - box.className = 'message ' + cssClass; + const box = document.getElementById("message"); + box.className = "message " + cssClass; box.textContent = message; - box.style.display = 'block'; + box.style.display = "block"; } export function hide(id) { - if(!canDisplay()) return; + if (!canDisplay()) { + return; + } - let box = document.getElementById(id); - box.style.display = 'none'; + const box = document.getElementById(id); + box.style.display = "none"; } export function show(id, node) { - if(!canDisplay()) return; + if (!canDisplay()) { + return null; + } - let box = document.getElementById(id); - box.style.display = 'block'; + const box = document.getElementById(id); + box.style.display = "block"; if (node) { box.replaceWith(node); } @@ -365,9 +390,16 @@ export function show(id, node) { } export function render(template, data, target) { - var output = Mustache.render(document.getElementById(template).innerHTML, data); - let box = document.getElementById(target); + const output = Mustache.render( + document.getElementById(template).innerHTML, + data + ); + const box = document.getElementById(target); + + // The innerHTML check is disabled because we trust Mustache output + // eslint-disable-next-line no-unsanitized/property box.innerHTML = output; - box.style.display = 'block'; + + box.style.display = "block"; return box; } diff --git a/frontend/src/index.js b/frontend/src/index.js index 352ba750f..4ef25afef 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,97 +1,113 @@ -import {REV_LATEST, DOM_READY, main, show, hide, message, get_path_coverage, get_history, get_zero_coverage_data, build_navbar, render, get_source, get_filters} from './common.js'; -import {buildRoute, readRoute, updateRoute} from './route.js'; -import {zero_coverage_display, zero_coverage_menu} from './zero_coverage_report.js'; -import './style.css'; -import Prism from 'prismjs'; -import Chartist from 'chartist'; -import 'chartist/dist/chartist.css'; - -const VIEW_ZERO_COVERAGE = 'zero'; -const VIEW_BROWSER = 'browser'; - - -function browser_menu(revision, filters, route) { - let context = { +import { + REV_LATEST, + DOM_READY, + main, + show, + hide, + message, + getPathCoverage, + getHistory, + getZeroCoverageData, + buildNavbar, + render, + getSource, + getFilters +} from "./common.js"; +import { buildRoute, readRoute, updateRoute } from "./route.js"; +import { + zeroCoverageDisplay, + zeroCoverageMenu +} from "./zero_coverage_report.js"; +import "./style.css"; +import Prism from "prismjs"; +import Chartist from "chartist"; +import "chartist/dist/chartist.css"; + +const VIEW_ZERO_COVERAGE = "zero"; +const VIEW_BROWSER = "browser"; + +function browserMenu(revision, filters, route) { + const context = { revision, - platforms: filters.platforms.map((p) => { + platforms: filters.platforms.map(p => { return { - 'name': p, - 'selected': p == route.platform, - } + name: p, + selected: p === route.platform + }; }), - suites: filters.suites.map((s) => { + suites: filters.suites.map(s => { return { - 'name': s, - 'selected': s == route.suite, - } - }), + name: s, + selected: s === route.suite + }; + }) }; - render('menu_browser', context, 'menu'); + render("menu_browser", context, "menu"); } async function graphHistory(history, path) { if (history === null) { - message('warning', `No history data for ${path}`); + message("warning", `No history data for ${path}`); return; } - let dateStr = function(timestamp){ - let date = new Date(timestamp); + const dateStr = function(timestamp) { + const date = new Date(timestamp); return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`; - } + }; var data = { series: [ { - name: 'History', + name: "History", data: history.map(push => { return { x: push.date * 1000, - y: push.coverage, - } + y: push.coverage + }; }) } - ], + ] }; var config = { // Display dates on a linear scale axisX: { type: Chartist.FixedScaleAxis, divisor: 20, - labelInterpolationFnc: dateStr, + labelInterpolationFnc: dateStr }, // Fix display bug when points are too close lineSmooth: Chartist.Interpolation.cardinal({ - tension: 1, - }), + tension: 1 + }) }; - let elt = show('history').querySelector('.ct-chart'); - let chart = new Chartist.Line(elt, data, config); + const elt = show("history").querySelector(".ct-chart"); + const chart = new Chartist.Line(elt, data, config); - chart.on('draw', function(evt) { - if(evt.type === 'point') { + chart.on("draw", function(evt) { + if (evt.type === "point") { // Load revision from graph when a point is clicked - let revision = history[evt.index].changeset; - evt.element._node.onclick = function(){ - updateRoute({revision}); + const revision = history[evt.index].changeset; + evt.element._node.onclick = function() { + updateRoute({ revision }); }; // Display revision from graph when a point is overed - evt.element._node.onmouseover = function(){ - let ctx = { + evt.element._node.onmouseover = function() { + const ctx = { revision: revision.substring(0, 12), - date: dateStr(evt.value.x), + date: dateStr(evt.value.x) }; - render('history_point', ctx, 'history_details'); + render("history_point", ctx, "history_details"); }; } }); } async function showDirectory(dir, revision, files) { - let context = { - navbar: build_navbar(dir, revision), + const context = { + navbar: buildNavbar(dir, revision), files: files.map(file => { file.route = buildRoute({ path: file.path @@ -99,87 +115,93 @@ async function showDirectory(dir, revision, files) { return file; }), revision: revision || REV_LATEST, - file_name: function(){ + file_name() { // Build filename relative to current dir - return dir ? this.path.substring(dir.length+1) : this.path; + return dir ? this.path.substring(dir.length + 1) : this.path; } }; - render('browser', context, 'output'); + render("browser", context, "output"); } async function showFile(file, revision) { - let source = await get_source(file.path); + const source = await getSource(file.path); let language; - if (file.path.endsWith('cpp') || file.path.endsWith('h')) { - language = 'cpp'; - } else if (file.path.endsWith('c')) { - language = 'c'; - } else if (file.path.endsWith('js') || file.path.endsWith('jsm')) { - language = 'javascript'; - } else if (file.path.endsWith('css')) { - language = 'css'; - } else if (file.path.endsWith('py')) { - language = 'python'; - } else if (file.path.endsWith('java')) { - language = 'java'; + if (file.path.endsWith("cpp") || file.path.endsWith("h")) { + language = "cpp"; + } else if (file.path.endsWith("c")) { + language = "c"; + } else if (file.path.endsWith("js") || file.path.endsWith("jsm")) { + language = "javascript"; + } else if (file.path.endsWith("css")) { + language = "css"; + } else if (file.path.endsWith("py")) { + language = "python"; + } else if (file.path.endsWith("java")) { + language = "java"; } - let context = { - navbar: build_navbar(file.path, revision), + const context = { + navbar: buildNavbar(file.path, revision), revision: revision || REV_LATEST, - language: language, - lines: source.split('\n').map((line, nb) => { - let coverage = file.coverage[nb]; - let css_class = ''; + language, + lines: source.split("\n").map((line, nb) => { + const coverage = file.coverage[nb]; + let cssClass = ""; if (coverage !== -1) { - css_class = coverage > 0 ? 'covered': 'uncovered'; + cssClass = coverage > 0 ? "covered" : "uncovered"; } return { - nb: nb, - line: line || ' ', - covered: css_class, - } - }), + nb, + line: line || " ", + covered: cssClass + }; + }) }; - hide('message'); - hide('history'); - let output = render('file_coverage', context, 'output'); + hide("message"); + hide("history"); + const output = render("file_coverage", context, "output"); // Highlight source code once displayed Prism.highlightAll(output); } async function load() { - let route = readRoute(); + const route = readRoute(); // Reset display, dom-safe - hide('history'); - hide('output'); - message('loading', 'Loading coverage data for ' + (route.path || 'mozilla-central') + ' @ ' + (route.revision || REV_LATEST)); + hide("history"); + hide("output"); + message( + "loading", + "Loading coverage data for " + + (route.path || "mozilla-central") + + " @ " + + (route.revision || REV_LATEST) + ); // Load only zero coverage for that specific view if (route.view === VIEW_ZERO_COVERAGE) { - let zero_coverage = await get_zero_coverage_data(); + const zeroCoverage = await getZeroCoverageData(); return { view: VIEW_ZERO_COVERAGE, path: route.path, - zero_coverage, - route, - } + zeroCoverage, + route + }; } try { var [coverage, history, filters] = await Promise.all([ - get_path_coverage(route.path, route.revision, route.platform, route.suite), - get_history(route.path, route.platform, route.suite), - get_filters(), + getPathCoverage(route.path, route.revision, route.platform, route.suite), + getHistory(route.path, route.platform, route.suite), + getFilters() ]); } catch (err) { - console.warn('Failed to load coverage', err); + console.warn("Failed to load coverage", err); await DOM_READY; // We want to always display this message - message('error', 'Failed to load coverage: ' + err.message); + message("error", "Failed to load coverage: " + err.message); throw err; } @@ -190,33 +212,28 @@ async function load() { route, coverage, history, - filters, + filters }; } async function display(data) { - - if (data.view === VIEW_ZERO_COVERAGE ) { - await zero_coverage_menu(data.route); - await zero_coverage_display(data.zero_coverage, data.path); - + if (data.view === VIEW_ZERO_COVERAGE) { + await zeroCoverageMenu(data.route); + await zeroCoverageDisplay(data.zeroCoverage, data.path); } else if (data.view === VIEW_BROWSER) { - browser_menu(data.revision, data.filters, data.route); + browserMenu(data.revision, data.filters, data.route); - if (data.coverage.type === 'directory') { - hide('message'); + if (data.coverage.type === "directory") { + hide("message"); await graphHistory(data.history, data.path); await showDirectory(data.path, data.revision, data.coverage.children); - - } else if (data.coverage.type === 'file') { + } else if (data.coverage.type === "file") { await showFile(data.coverage, data.revision); - } else { - message('error', 'Invalid file type: ' + data.coverate.type); + message("error", "Invalid file type: " + data.coverate.type); } - } else { - message('error', 'Invalid view : ' + data.view); + message("error", "Invalid view : " + data.view); } } diff --git a/frontend/src/route.js b/frontend/src/route.js index 518645496..58eb2de0b 100644 --- a/frontend/src/route.js +++ b/frontend/src/route.js @@ -1,14 +1,14 @@ -import {REV_LATEST} from './common.js'; +import { REV_LATEST } from "./common.js"; export function readRoute() { // Reads all filters from current URL hash - let hash = window.location.hash.substring(1); - let pairs = hash.split('&'); - let out = {} + const hash = window.location.hash.substring(1); + const pairs = hash.split("&"); + const out = {}; pairs.forEach(pair => { - let [key, value] = pair.split('='); - if(!key) { - return + const [key, value] = pair.split("="); + if (!key) { + return; } out[decodeURIComponent(key)] = decodeURIComponent(value); }); @@ -18,7 +18,7 @@ export function readRoute() { out.revision = REV_LATEST; } if (!out.path) { - out.path = ''; + out.path = ""; } return out; @@ -28,13 +28,16 @@ export function buildRoute(params) { // Add all params on top of current route let route = readRoute(); if (params) { - route = {...route, ...params} + route = { ...route, ...params }; } // Build query string from filters - return '#' + Object.keys(route) - .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(route[k])) - .join('&'); + return ( + "#" + + Object.keys(route) + .map(k => encodeURIComponent(k) + "=" + encodeURIComponent(route[k])) + .join("&") + ); } export function updateRoute(params) { diff --git a/frontend/src/zero_coverage_report.js b/frontend/src/zero_coverage_report.js index dabc908ea..aeeeaba88 100644 --- a/frontend/src/zero_coverage_report.js +++ b/frontend/src/zero_coverage_report.js @@ -1,82 +1,95 @@ -import {hide, message, build_navbar, render, filter_third_party, filter_languages, filter_headers, filter_completely_uncovered, filter_last_push_date, is_enabled} from './common.js'; -import {buildRoute} from './route.js'; +import { + hide, + message, + buildNavbar, + isEnabled, + render, + filterThirdParty, + filterLanguages, + filterHeaders, + filterCompletelyUncovered, + filterLastPushDate +} from "./common.js"; +import { buildRoute } from "./route.js"; export const ZERO_COVERAGE_FILTERS = { - 'third_party': { + third_party: { name: "Show third-party files", - default_value: 'on', + default_value: "on" }, - 'headers': { - name: 'Show headers', - default_value: 'off', + headers: { + name: "Show headers", + default_value: "off" }, - 'completely_uncovered': { - name: 'Show completely uncovered files only', - default_value: 'off', + completely_uncovered: { + name: "Show completely uncovered files only", + default_value: "off" }, - 'cpp': { - name: 'C/C++', - default_value: 'on', + cpp: { + name: "C/C++", + default_value: "on" }, - 'js': { - name: 'JavaScript', - default_value: 'on', + js: { + name: "JavaScript", + default_value: "on" }, - 'java': { - name: 'Java', - default_value: 'on', - }, - 'rust': { - name: 'Rust', - default_value: 'on', + java: { + name: "Java", + default_value: "on" }, + rust: { + name: "Rust", + default_value: "on" + } }; const ZERO_COVERAGE_PUSHES = { - 'all': 'All', - 'one_year': '0 < 1 year', - 'two_years': '1 < 2 years', - 'older_than_two_years': 'Older than 2 years', -} - + all: "All", + one_year: "0 < 1 year", + two_years: "1 < 2 years", + older_than_two_years: "Older than 2 years" +}; -export function zero_coverage_menu(route){ - let context = { +export function zeroCoverageMenu(route) { + const context = { filters: Object.entries(ZERO_COVERAGE_FILTERS).map(([key, filter]) => { return { key, message: filter.name, - checked: is_enabled(key), - } + checked: isEnabled(key) + }; }), - last_pushes: Object.entries(ZERO_COVERAGE_PUSHES).map(([value, message]) => { - return { - value, - message, - selected: route['last_push'] === value, + last_pushes: Object.entries(ZERO_COVERAGE_PUSHES).map( + ([value, message]) => { + return { + value, + message, + selected: route.last_push === value + }; } - }), + ) }; - render('menu_zero', context, 'menu'); + render("menu_zero", context, "menu"); } +function sortEntries(entries) { + return entries + .sort(([dir1, stats1], [dir2, stats2]) => { + if (stats1.children !== stats2.children) { + return stats1.children < stats2.children; + } -function sort_entries(entries) { - return entries.sort(([dir1, stats1], [dir2, stats2]) => { - if (stats1.children != stats2.children) { - return stats1.children < stats2.children; - } - - if (stats1.funcs != stats2.funcs) { - return stats1.funcs < stats2.funcs; - } + if (stats1.funcs !== stats2.funcs) { + return stats1.funcs < stats2.funcs; + } - return dir1 > dir2; - }).map(([dir , stats]) => { - return {stats, dir}; - }); + return dir1 > dir2; + }) + .map(([dir, stats]) => { + return { stats, dir }; + }); } -function get_min_date(oldDate, newDate) { +function getMinDate(oldDate, newDate) { if (!oldDate) { return newDate; } @@ -88,12 +101,14 @@ function get_min_date(oldDate, newDate) { } function getBaseStats(file, children) { - return {'children': children, - 'funcs': file.funcs, - 'first_push_date': file.first_push_date, - 'last_push_date': file.last_push_date, - 'size': file.size, - 'commits': file.commits}; + return { + children, + funcs: file.funcs, + first_push_date: file.first_push_date, + lastPushDate: file.lastPushDate, + size: file.size, + commits: file.commits + }; } function cumStats(prevStats, newStats) { @@ -101,48 +116,50 @@ function cumStats(prevStats, newStats) { prevStats.funcs += newStats.funcs; prevStats.size += newStats.size; prevStats.commits += newStats.commits; - prevStats.first_push_date = get_min_date(prevStats.first_push_date, newStats.first_push_date); - prevStats.last_push_date = get_min_date(prevStats.last_push_date, newStats.last_push_date); + prevStats.first_push_date = getMinDate( + prevStats.first_push_date, + newStats.first_push_date + ); + prevStats.lastPushDate = getMinDate( + prevStats.lastPushDate, + newStats.lastPushDate + ); } -function getFileSize(size) { - if (size >= 1e6) { - return (size / 1e6).toFixed(2) + 'M'; - } else if (size >= 1e3) { - return (size / 1e3).toFixed(1) + 'K'; - } - return size; -} - -export async function zero_coverage_display(data, dir) { - hide('output'); - hide('history'); - message('loading', 'Loading zero coverage report for ' + (dir || 'mozilla-central')); +export async function zeroCoverageDisplay(data, dir) { + hide("output"); + hide("history"); + message( + "loading", + "Loading zero coverage report for " + (dir || "mozilla-central") + ); - while (dir.endsWith('/')) dir = dir.substring(0, dir.length - 1); - dir += '/'; - if (dir == '/') { - dir = ''; + while (dir.endsWith("/")) { + dir = dir.substring(0, dir.length - 1); + } + dir += "/"; + if (dir === "/") { + dir = ""; } - let files = data['files'].filter(file => file.name.startsWith(dir)); + let files = data.files.filter(file => file.name.startsWith(dir)); // TODO: Do this in the backend directly! files.forEach(file => { file.path = file.name; }); - files = await filter_third_party(files); - files = filter_languages(files); - files = filter_headers(files); - files = filter_completely_uncovered(files); - files = filter_last_push_date(files); + files = await filterThirdParty(files); + files = filterLanguages(files); + files = filterHeaders(files); + files = filterCompletelyUncovered(files); + files = filterLastPushDate(files); - let map = new Map(); + const map = new Map(); - for (let file of files) { - let rest = file.path.substring(dir.lastIndexOf('/') + 1); + for (const file of files) { + let rest = file.path.substring(dir.lastIndexOf("/") + 1); - if (rest.includes('/')) { - rest = rest.substring(0, rest.indexOf('/')); + if (rest.includes("/")) { + rest = rest.substring(0, rest.indexOf("/")); if (map.has(rest)) { cumStats(map.get(rest), file); } else { @@ -150,32 +167,31 @@ export async function zero_coverage_display(data, dir) { } } else { if (map.has(rest)) { - console.warn(rest + ' is already in map.'); + console.warn(rest + " is already in map."); } map.set(rest, getBaseStats(file, 0)); } } - const revision = data['hg_revision']; - let context = { + const revision = data.hg_revision; + const context = { current_dir: dir, - entries: sort_entries(Array.from(map.entries())), - entry_url : function() { - let path = dir + this.dir; - if (this.stats.children != 0) { + entries: sortEntries(Array.from(map.entries())), + entry_url() { + const path = dir + this.dir; + if (this.stats.children !== 0) { return buildRoute({ - view: 'zero', - path, - }) - } else { - // Fully reset the url when moving back to browser view - return `#view=browser&revision=${revision}&path=${path}`; + view: "zero", + path + }); } + // Fully reset the url when moving back to browser view + return `#view=browser&revision=${revision}&path=${path}`; }, - navbar: build_navbar(dir), - total: files.length, + navbar: buildNavbar(dir), + total: files.length }; - hide('message'); - render('zerocoverage', context, 'output'); + hide("message"); + render("zerocoverage", context, "output"); } diff --git a/frontend/webpack.common.js b/frontend/webpack.common.js index 164e15e69..c48db7b7b 100644 --- a/frontend/webpack.common.js +++ b/frontend/webpack.common.js @@ -25,6 +25,12 @@ module.exports = { ], module: { rules: [ + { + enforce: 'pre', + test: /\.js$/, + exclude: /node_modules/, + loader: 'eslint-loader', + }, { test: /\.js$/, exclude: /(node_modules)/,