Skip to content

Commit 0e692aa

Browse files
authored
New Rule: enforce-min-coverage-comments-sync (#277)
* initial updateCommentThreshold * on second thought it should be separate rule enforce-min-coverage-comments-sync * create not under meta * fix func name * introduce getMinCoverageDirectiveCommentNodeAndPercent * fix works!? TODO: how to revert test file after spec fixes it? * 4.5.0-pre0.1 * floor * remove comment if coverage above global threshold * add coverage-sync-* examples * add to codebases * Update format.spec.js.snap * handle expected fixes * no js ext on fixed examples so they won't be fixed themselves * percent is 33 apparently ok flow * version * remove leftover copypasta from enforce-min-coverage * 0.4 * address pr review comments
1 parent cbaa467 commit 0e692aa

File tree

10 files changed

+147
-5
lines changed

10 files changed

+147
-5
lines changed

.npmignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
src/
2+
flow-typed/
3+
test/

src/index.js

+69
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,24 @@ function createFilteredErrorRule(filter: (CollectOutputElement) => any): (contex
155155
};
156156
}
157157

158+
const MIN_COVERAGE_DIRECTIVE_COMMENT_PATTERN =
159+
/(\s*eslint\s*['"]flowtype-errors\/enforce-min-coverage['"]\s*:\s*\[\s*(?:2|['"]error['"])\s*,\s*)(\d+)(\]\s*)/
160+
161+
function getMinCoverageDirectiveCommentNodeAndPercent(sourceCode) {
162+
let commentNode
163+
let minPercent
164+
// eslint-disable-next-line no-restricted-syntax
165+
for (const comment of sourceCode.getAllComments()) {
166+
const match = comment.value.match(MIN_COVERAGE_DIRECTIVE_COMMENT_PATTERN)
167+
if (match && match[2]) {
168+
commentNode = comment
169+
minPercent = parseInt(match[2], 10)
170+
break
171+
}
172+
}
173+
return [commentNode, minPercent]
174+
}
175+
158176
const getCoverage = (context, node) => {
159177
const source = context.getSourceCode();
160178
const info = lookupInfo(context, source, node);
@@ -242,6 +260,57 @@ export default {
242260
},
243261
};
244262
},
263+
'enforce-min-coverage-comments-sync': {
264+
meta: {
265+
fixable: 'code',
266+
},
267+
create: function enforceMinCoverageCommentsSync(
268+
context: EslintContext
269+
): ReturnRule {
270+
return {
271+
Program(node: Object) {
272+
const res = getCoverage(context, node);
273+
if (!res) {
274+
return;
275+
}
276+
277+
const sourceCode = context.getSourceCode()
278+
const [minCoverageDirectiveCommentNode, requiredCoverage] = getMinCoverageDirectiveCommentNodeAndPercent(sourceCode)
279+
if (!minCoverageDirectiveCommentNode || !requiredCoverage) {
280+
return;
281+
}
282+
283+
// Get global requiredCoverage outside the inline module comment.
284+
const enforceMinCoverage = context.options[0];
285+
// If flow coverage is >=updateCommentThreshold% greater than allowed, update the eslint comment.
286+
const updateCommentThreshold = context.options[1];
287+
const { coveredCount, uncoveredCount } = res.coverageInfo;
288+
289+
/* eslint prefer-template: 0 */
290+
const percentage = Number(
291+
Math.round(
292+
(coveredCount / (coveredCount + uncoveredCount)) * 10000
293+
) + 'e-2'
294+
);
295+
296+
if (percentage - requiredCoverage > updateCommentThreshold) {
297+
context.report({
298+
loc: res.program.loc,
299+
message: `Expected coverage comment to be within ${updateCommentThreshold}% of ${requiredCoverage}%, but is: ${percentage}%`,
300+
fix(fixer) {
301+
if (percentage >= enforceMinCoverage) {
302+
// If coverage >= global required amount, remove comment entirely.
303+
return fixer.replaceText(minCoverageDirectiveCommentNode, '')
304+
}
305+
306+
return fixer.replaceText(minCoverageDirectiveCommentNode, minCoverageDirectiveCommentNode.value.replace(MIN_COVERAGE_DIRECTIVE_COMMENT_PATTERN, `/*$1${Math.floor(percentage)}$3*/`))
307+
}
308+
});
309+
}
310+
},
311+
};
312+
},
313+
},
245314
'show-errors': (createFilteredErrorRule(
246315
({ level }) => level !== FlowSeverity.Warning
247316
) : (context: EslintContext) => ReturnRule),

test/__snapshots__/format.spec.js.snap

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ exports[`Check codebases coverage-ok - eslint should give expected output 1`] =
3333

3434
exports[`Check codebases coverage-ok2 - eslint should give expected output 1`] = `""`;
3535

36+
exports[`Check codebases coverage-sync-remove - eslint should give expected output 1`] = `""`;
37+
38+
exports[`Check codebases coverage-sync-update - eslint should give expected output 1`] = `""`;
39+
3640
exports[`Check codebases flow-pragma-1 - eslint should give expected output 1`] = `
3741
"
3842
./example.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[options]
2+
esproposal.class_static_fields=enable
3+
esproposal.class_instance_fields=enable
4+
esproposal.export_star_as=enable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @flow
2+
3+
4+
let x: number = 100;
5+
let x2: number = 100;
6+
let x3: number = 100;
7+
let x4;
8+
let x5;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @flow
2+
/* eslint "flowtype-errors/enforce-min-coverage": [2, 30] */
3+
4+
let x: number = 100;
5+
let x2: number = 100;
6+
let x3: number = 100;
7+
let x4;
8+
let x5;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[options]
2+
esproposal.class_static_fields=enable
3+
esproposal.class_instance_fields=enable
4+
esproposal.export_star_as=enable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @flow
2+
/* eslint "flowtype-errors/enforce-min-coverage": [2, 33] */
3+
4+
let x: number = 100;
5+
let x2;
6+
let x3;
7+
let x4;
8+
let x5;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @flow
2+
/* eslint "flowtype-errors/enforce-min-coverage": [2, 5] */
3+
4+
let x: number = 100;
5+
let x2;
6+
let x3;
7+
let x4;
8+
let x5;

test/format.spec.js

+31-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1+
/* eslint-disable no-nested-ternary */
12
import path from 'path';
23
import { expect as chaiExpect } from 'chai';
3-
import { readFileSync, writeFileSync, unlinkSync } from 'fs';
4+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
45
// $FlowIgnore
56
import execa from 'execa';
67
import { collect } from '../src/collect';
@@ -60,7 +61,7 @@ describe('Format', () => {
6061
const ESLINT_PATH = path.resolve('./node_modules/eslint/bin/eslint.js');
6162

6263
async function runEslint(cwd) {
63-
const result = await execa(ESLINT_PATH, ['**/*.{js,vue}'], { cwd, stripEof: false });
64+
const result = await execa(ESLINT_PATH, ['**/*.{js,vue}', '--fix'], { cwd, stripEof: false });
6465
result.stdout = result.stdout && result.stdout.toString();
6566
result.stderr = result.stderr && result.stderr.toString();
6667
return result;
@@ -72,6 +73,8 @@ const codebases = [
7273
'coverage-fail2',
7374
'coverage-ok',
7475
'coverage-ok2',
76+
'coverage-sync-remove',
77+
'coverage-sync-update',
7578
'flow-pragma-1',
7679
'flow-pragma-2',
7780
'html-support',
@@ -86,7 +89,7 @@ const codebases = [
8689
'uncovered-example'
8790
];
8891

89-
const eslintConfig = (enforceMinCoverage, checkUncovered, html) => `
92+
const eslintConfig = (enforceMinCoverage, updateCommentThreshold, checkUncovered, html) => `
9093
const Module = require('module');
9194
const path = require('path');
9295
const original = Module._resolveFilename;
@@ -119,8 +122,15 @@ const eslintConfig = (enforceMinCoverage, checkUncovered, html) => `
119122
}
120123
},
121124
rules: {
122-
${enforceMinCoverage
123-
? `'flowtype-errors/enforce-min-coverage': [2, ${enforceMinCoverage}],` : ``}
125+
${updateCommentThreshold
126+
? [
127+
`'flowtype-errors/enforce-min-coverage': [2, ${enforceMinCoverage}],`,
128+
`'flowtype-errors/enforce-min-coverage-comments-sync': [2, ${enforceMinCoverage}, ${updateCommentThreshold}],`,
129+
].join('\n')
130+
: enforceMinCoverage
131+
? `'flowtype-errors/enforce-min-coverage': [2, ${enforceMinCoverage}],`
132+
: ``
133+
}
124134
${checkUncovered ? `'flowtype-errors/uncovered': 2,` : ''}
125135
'flowtype-errors/show-errors': 2,
126136
'flowtype-errors/show-warnings': 1
@@ -144,13 +154,20 @@ describe('Check codebases', () => {
144154
// eslint-disable-next-line no-loop-func
145155
it(`${title} - eslint should give expected output`, async() => {
146156
const fullFolder = path.resolve(`./test/codebases/${folder}`);
157+
const exampleJsFilePath = path.resolve(`./test/codebases/${folder}/example.js`);
158+
const exampleJsFixedFilePath = path.resolve(`./test/codebases/${folder}/example.fixed`);
159+
const hasFix = existsSync(exampleJsFixedFilePath)
147160
const configPath = path.resolve(fullFolder, '.eslintrc.js');
148161

162+
const contentsBefore = hasFix && readFileSync(exampleJsFilePath, 'utf8')
163+
const contentsExpected = hasFix && readFileSync(exampleJsFixedFilePath, 'utf8')
164+
149165
// Write config file
150166
writeFileSync(
151167
configPath,
152168
eslintConfig(
153169
folder.match(/^coverage-/) ? 50 : 0,
170+
folder.match(/^coverage-sync/) ? 10 : 0,
154171
!!folder.match(/^uncovered-/),
155172
/html-support/.test(folder)
156173
)
@@ -164,6 +181,11 @@ describe('Check codebases', () => {
164181
'gm'
165182
); // Escape regexp
166183

184+
const contentsAfter = hasFix && readFileSync(exampleJsFilePath, 'utf8')
185+
186+
// Revert the file to before it was fixed, since this file is checked into git.
187+
if (hasFix && contentsBefore !== contentsAfter) writeFileSync(exampleJsFilePath, contentsBefore)
188+
167189
// Strip root from filenames
168190
expect(
169191
stdout.replace(regexp, match =>
@@ -173,6 +195,10 @@ describe('Check codebases', () => {
173195

174196
expect(stderr).toEqual('');
175197

198+
if (hasFix) {
199+
expect(contentsAfter).toEqual(contentsExpected);
200+
}
201+
176202
// Clean up
177203
unlinkSync(configPath);
178204
});

0 commit comments

Comments
 (0)