eslint-plugin-ft-flow
Version:
Flowtype linting rules for ESLint by flow-typed
157 lines (132 loc) • 4.78 kB
Flow
import _ from 'lodash';
import {
isFlowFileAnnotation,
fuzzyStringMatch,
} from '../utilities';
const defaults = {
annotationStyle: 'none',
strict: false,
};
const looksLikeFlowFileAnnotation = (comment) => /@(?:no)?flo/ui.test(comment);
const isValidAnnotationStyle = (node, style) => {
if (style === 'none') {
return true;
}
return style === node.type.toLowerCase();
};
const checkAnnotationSpelling = (comment) => /@[a-z]+\b/u.test(comment) && fuzzyStringMatch(comment.replace(/no/ui, ''), '@flow', 0.2);
const isFlowStrict = (comment) => /^@flow\sstrict\b/u.test(comment);
const noFlowAnnotation = (comment) => /^@noflow\b/u.test(comment);
const schema = [
{
enum: ['always', 'never'],
type: 'string',
},
{
additionalProperties: false,
properties: {
annotationStyle: {
enum: ['none', 'line', 'block'],
type: 'string',
},
strict: {
enum: [true, false],
type: 'boolean',
},
},
type: 'object',
},
];
const create = (context) => {
const always = context.options[0] === 'always';
const style = _.get(context, 'options[1].annotationStyle', defaults.annotationStyle);
const flowStrict = _.get(context, 'options[1].strict', defaults.strict);
return {
Program(node) {
const firstToken = node.tokens[0];
const potentialFlowFileAnnotation = _.find(
context.getSourceCode().getAllComments(),
(comment) => looksLikeFlowFileAnnotation(comment.value),
);
if (potentialFlowFileAnnotation) {
if (firstToken && firstToken.range[0] < potentialFlowFileAnnotation.range[0]) {
context.report({ message: 'Flow file annotation not at the top of the file.', node: potentialFlowFileAnnotation });
}
const annotationValue = potentialFlowFileAnnotation.value.trim();
if (isFlowFileAnnotation(annotationValue)) {
if (!isValidAnnotationStyle(potentialFlowFileAnnotation, style)) {
const annotation = style === 'line' ? `// ${annotationValue}` : `/* ${annotationValue} */`;
context.report({
fix: (fixer) => fixer.replaceTextRange(
[
potentialFlowFileAnnotation.range[0],
potentialFlowFileAnnotation.range[1],
],
annotation,
),
message: `Flow file annotation style must be \`${annotation}\``,
node: potentialFlowFileAnnotation,
});
}
if (!noFlowAnnotation(annotationValue) && flowStrict && !isFlowStrict(annotationValue)) {
const str = style === 'line' ? '`// @flow strict`' : '`/* @flow strict */`';
context.report({
fix: (fixer) => {
const annotation = ['line', 'none'].includes(style) ? '// @flow strict' : '/* @flow strict */';
return fixer.replaceTextRange([
potentialFlowFileAnnotation.range[0],
potentialFlowFileAnnotation.range[1],
], annotation);
},
message: `Strict Flow file annotation is required, must be ${str}`,
node,
});
}
} else if (checkAnnotationSpelling(annotationValue)) {
context.report({ message: 'Misspelled or malformed Flow file annotation.', node: potentialFlowFileAnnotation });
} else {
context.report({ message: 'Malformed Flow file annotation.', node: potentialFlowFileAnnotation });
}
} else if (always && !_.get(context, 'settings[\'ft-flow\'].onlyFilesWithFlowAnnotation')) {
context.report({
fix: (fixer) => {
let annotation;
if (flowStrict) {
annotation = ['line', 'none'].includes(style) ? '// @flow strict\n' : '/* @flow strict */\n';
} else {
annotation = ['line', 'none'].includes(style) ? '// @flow\n' : '/* @flow */\n';
}
const firstComment = node.comments[0];
if (firstComment && firstComment.type === 'Shebang') {
return fixer
.replaceTextRange(
[
firstComment.range[1],
firstComment.range[1],
],
`\n${annotation.trim()}`,
);
}
return fixer
.replaceTextRange(
[
node.range[0],
node.range[0],
],
annotation,
);
},
message: 'Flow file annotation is missing.',
node,
});
}
},
};
};
export default {
create,
meta: {
fixable: 'code',
schema,
},
};