Skip to content

Instantly share code, notes, and snippets.

@nsfmc
Created April 17, 2017 23:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nsfmc/849fbf50bb264cfac7d383d41543f37d to your computer and use it in GitHub Desktop.
Save nsfmc/849fbf50bb264cfac7d383d41543f37d to your computer and use it in GitHub Desktop.
// @flow
import {parse} from 'babylon';
const commonParser = (input: string) => parse(input, {
sourceType: 'module',
plugins: ['jsx', 'flow'],
});
const bodyParser = (input: string) => commonParser(input).program.body;
const commentParser = (input: string) => commonParser(input).comments;
const parseLine = (input: string) => bodyParser(input).filter(
d => d.type === 'ImportDeclaration'
)[0];
const typeofImportDeclaration = (declaration: any) => {
const value = declaration.source.value;
const src = value.startsWith('src/');
const relative = value.search(/^\.+/) !== -1;
const cssPath = value.endsWith('.css');
const jsxPath = value.endsWith('.jsx');
const componentPath = value.search('components/') !== -1;
const flowImport = declaration.importKind === 'type';
if (flowImport) {
return 'type';
}
if (!src && !relative) {
return 'vendor';
}
if (cssPath) {
return 'css';
}
if (jsxPath || componentPath) {
return 'component';
}
if (src && !componentPath) {
return 'misc';
}
};
export const kindOf = (importString: string) => (
typeofImportDeclaration(parseLine(importString))
);
export const highLevelParser = (codeBlock: string) => {
const declarations = bodyParser(codeBlock).map(declaration => {
const startLine = declaration.loc.start.line;
const endLine = declaration.loc.end.line;
switch (declaration.type) {
case 'ImportDeclaration':
return [typeofImportDeclaration(declaration), [startLine, endLine]];
default:
return ['other', [startLine, endLine]];
}
});
const comments = commentParser(codeBlock).map(comment => {
const startLine = comment.loc.start.line;
const endLine = comment.loc.start.line;
return ['comment', [startLine, endLine], comment.value];
});
return [...declarations, ...comments].sort((a, b) => a[1][0] - b[1][0]);
};
export const isSpacedOk = (simpleParsed: any[]) => {
const pragmaFirst = pragmaSpaced(simpleParsed);
const twoSpaces = doubleSpacedContent(simpleParsed);
return pragmaFirst && twoSpaces;
};
const doubleSpacedContent = (simpleParsed: any[]) => {
// Ensure there are two spaces between import country and code land
const firstNonCommentLine = simpleParsed.findIndex(node => node[0] === 'other');
if (firstNonCommentLine > 0) {
const lastNonCodeDeclaration = simpleParsed[firstNonCommentLine - 1];
const lastImportishEndLine = lastNonCodeDeclaration[1][1];
const firstNonImportStartLine = simpleParsed[firstNonCommentLine][1][0];
return firstNonImportStartLine - lastImportishEndLine === 3;
}
return true; // pure vanilla js, don't care;
};
const pragmaSpaced = (simpleParsed: any[]) => {
// ensure that there is an empty line between pragma and non-pragma
const firstNonComment = simpleParsed.findIndex(node => node[0] !== 'comment' && node[0] !== 'other');
const pragma = simpleParsed.findIndex(node => {
const [type, loc, value] = node;
const isComment = type === 'comment';
if (isComment) {
const isFlowPragma = value.search('@flow') !== -1;
return isFlowPragma;
}
return false;
});
const hasPragma = pragma > -1;
const pragmaFirst = hasPragma && pragma < firstNonComment;
if (pragmaFirst) {
const pragmaLine = simpleParsed[pragma][1][1];
const nextLine = simpleParsed[firstNonComment][1][0];
return nextLine - pragmaLine === 2;
}
return true; // no pragma, don't care
};
// @flow
import {kindOf, highLevelParser, isSpacedOk} from '../imports';
describe('detecting vendor imports', () => {
test('looks like a vendor import', () => {
expect(kindOf(`import React from 'react';`)).toEqual('vendor');
});
test('looks like a destructured vendor import', () => {
expect(kindOf(`import {Component} from 'react';`)).toEqual('vendor');
});
test('deals with lodash and other nested imports', () => {
expect(kindOf(`import zip from 'lodash/zip';`)).toEqual('vendor');
});
});
describe('detects flow type imports', () => {
test('looks like a flow type import', () => {
expect(kindOf(`import type {CSS} from './style.css';`)).toEqual('type');
});
});
describe('detects css imports', () => {
test('looks like a css import', () => {
expect(kindOf(`import css from './style.css';`)).toEqual('css');
});
});
describe('detects component imports', () => {
test('looks like a component import', () => {
expect(kindOf(`import Selector from 'src/components/lib/Selector/selector.jsx';`)).toEqual('component');
});
test('looks like a component import', () => {
expect(kindOf(`import Selector from 'src/components/lib/rte/';`)).toEqual('component');
});
});
describe('detects miscellaneous js imports', () => {
test('looks like a utils import', () => {
expect(kindOf(`import * as reduxApi from 'src/utils/redux-api.js';`)).toEqual('misc');
});
test('looks like an action-creator import', () => {
expect(kindOf(`import * as reduxApi from 'src/action-creators/agency-overvie';`)).toEqual('misc');
});
});
describe('highLevelParser', () => {
const codeBlock = (
`// @flow
import type {AgencyOverview} from 'src/api-parsers/index';
import flow from 'lodash/flow';
import {key, cached, fetching} from 'src/utils/redux';
import * as reduxApi from 'src/utils/redux-api.js';
import parsers from 'src/api-parsers/index'; // eslint-disable-line no-duplicate-imports
import css from './blah.css';
export const FOO = 2;
`);
test('declarationOrder', () => {
expect(highLevelParser(codeBlock)[0]).toEqual(['comment', [1, 1], ' @flow']);
});
});
describe('spacing', () => {
test('complains about inadequate space between pragma and first import', () => {
const parsed = [['comment', [1, 1], ' @flow'], ['type', [2, 2]], ['vendor', [4, 4]]];
expect(isSpacedOk(parsed)).toBeFalsy();
});
test('is ok with one line between pragma and first import', () => {
const parsed = [['comment', [1, 1], ' @flow'], ['type', [3, 2]], ['vendor', [4, 4]]];
expect(isSpacedOk(parsed)).toBeTruthy();
});
test('needs two lines between pragma and first non-import node', () => {
const parsed = [['comment', [1, 1], ' @flow'], ['other', [4, 2]]];
expect(isSpacedOk(parsed)).toBeTruthy();
});
test('needs two lines between imports and first non-import node', () => {
const parsed = [['comment', [1, 1], 'whatever'], ['css', [6, 9]], ['other', [12, 12]]];
expect(isSpacedOk(parsed)).toBeTruthy();
});
test('deals with files without imports', () => {
const parsed = [['other', [2, 2]]];
expect(isSpacedOk(parsed)).toBeTruthy();
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment