main.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. const levenshtein = require('js-levenshtein');
  2. const DiffMatchPatch = require('diff-match-patch');
  3. const dmp = new DiffMatchPatch();
  4. const dedent = require('dedent');
  5. const { walk, EXCLUDE_LISTS } = require('file-walker');
  6. const { CommentParser } = require('../comment-parser/main');
  7. const fs = require('fs');
  8. const path_ = require('path');
  9. const CompareFn = ({ header1, header2, distance_only = false }) => {
  10. // Calculate Levenshtein distance
  11. const distance = levenshtein(header1, header2);
  12. // console.log(`Levenshtein distance: ${distance}`);
  13. if ( distance_only ) return { distance };
  14. // Generate diffs using diff-match-patch
  15. const diffs = dmp.diff_main(header1, header2);
  16. dmp.diff_cleanupSemantic(diffs);
  17. let term_diff = '';
  18. // Manually format diffs for terminal display
  19. diffs.forEach(([type, text]) => {
  20. switch (type) {
  21. case DiffMatchPatch.DIFF_INSERT:
  22. term_diff += `\x1b[32m${text}\x1b[0m`; // Green for insertions
  23. break;
  24. case DiffMatchPatch.DIFF_DELETE:
  25. term_diff += `\x1b[31m${text}\x1b[0m`; // Red for deletions
  26. break;
  27. case DiffMatchPatch.DIFF_EQUAL:
  28. term_diff += text; // No color for equal parts
  29. break;
  30. }
  31. });
  32. return {
  33. distance,
  34. term_diff,
  35. };
  36. }
  37. const LicenseChecker = ({
  38. comment_parser,
  39. desired_header,
  40. }) => {
  41. const supports = ({ filename }) => {
  42. return comment_parser.supports({ filename });
  43. };
  44. const compare = async ({ filename, source }) => {
  45. const headers = await comment_parser.extract_top_comments(
  46. { filename, source });
  47. const headers_lines = headers.map(h => h.lines);
  48. if ( headers.length < 1 ) {
  49. return {
  50. has_header: false,
  51. };
  52. }
  53. // console.log('headers', headers);
  54. let top = 0;
  55. let bottom = 0;
  56. let current_distance = Number.MAX_SAFE_INTEGER;
  57. // "wah"
  58. for ( let i=1 ; i <= headers.length ; i++ ) {
  59. const combined = headers_lines.slice(top, i).flat();
  60. const combined_txt = combined.join('\n');
  61. const { distance } =
  62. CompareFn({
  63. header1: desired_header,
  64. header2: combined_txt,
  65. distance_only: true,
  66. });
  67. if ( distance < current_distance ) {
  68. current_distance = distance;
  69. bottom = i;
  70. } else {
  71. break;
  72. }
  73. }
  74. // "woop"
  75. for ( let i=1 ; i < headers.length ; i++ ) {
  76. const combined = headers_lines.slice(i, bottom).flat();
  77. const combined_txt = combined.join('\n');
  78. const { distance } =
  79. CompareFn({
  80. header1: desired_header,
  81. header2: combined_txt,
  82. distance_only: true,
  83. });
  84. if ( distance < current_distance ) {
  85. current_distance = distance;
  86. top = i;
  87. } else {
  88. break;
  89. }
  90. }
  91. const combined = headers_lines.slice(top, bottom).flat();
  92. const combined_txt = combined.join('\n');
  93. const diff_info = CompareFn({
  94. header1: desired_header,
  95. header2: combined_txt,
  96. })
  97. diff_info.range = [
  98. headers[top].range[0],
  99. headers[bottom-1].range[1],
  100. ];
  101. diff_info.has_header = true;
  102. return diff_info;
  103. };
  104. return {
  105. compare,
  106. supports,
  107. };
  108. };
  109. const license_check_test = async ({ options }) => {
  110. const comment_parser = CommentParser();
  111. const license_checker = LicenseChecker({
  112. comment_parser,
  113. desired_header: fs.readFileSync(
  114. path_.join(__dirname, '../../doc/license_header.txt'),
  115. 'utf-8',
  116. ),
  117. });
  118. const walk_iterator = walk({
  119. excludes: EXCLUDE_LISTS.NOT_SOURCE,
  120. }, path_.join(__dirname, '../..'));
  121. for await ( const value of walk_iterator ) {
  122. if ( value.is_dir ) continue;
  123. if ( value.name !== 'dev-console-ui-utils.js' ) continue;
  124. console.log(value.path);
  125. const source = fs.readFileSync(value.path, 'utf-8');
  126. const diff_info = await license_checker.compare({
  127. filename: value.name,
  128. source,
  129. })
  130. if ( diff_info ) {
  131. process.stdout.write('\x1B[36;1m=======\x1B[0m\n');
  132. process.stdout.write(diff_info.term_diff);
  133. process.stdout.write('\n\x1B[36;1m=======\x1B[0m\n');
  134. // console.log('headers', headers);
  135. } else {
  136. console.log('NO COMMENT');
  137. }
  138. console.log('RANGE', diff_info.range)
  139. const new_comment = comment_parser.output_comment({
  140. filename: value.name,
  141. style: 'block',
  142. text: 'some text\nto display'
  143. });
  144. console.log('NEW COMMENT?', new_comment);
  145. }
  146. };
  147. const cmd_check_fn = async () => {
  148. const comment_parser = CommentParser();
  149. const license_checker = LicenseChecker({
  150. comment_parser,
  151. desired_header: fs.readFileSync(
  152. path_.join(__dirname, '../../doc/license_header.txt'),
  153. 'utf-8',
  154. ),
  155. });
  156. const counts = {
  157. ok: 0,
  158. missing: 0,
  159. conflict: 0,
  160. error: 0,
  161. unsupported: 0,
  162. };
  163. const walk_iterator = walk({
  164. excludes: EXCLUDE_LISTS.NOT_SOURCE,
  165. }, path_.join(__dirname, '../..'));
  166. for await ( const value of walk_iterator ) {
  167. if ( value.is_dir ) continue;
  168. process.stdout.write(value.path + ' ... ');
  169. if ( ! license_checker.supports({ filename: value.name }) ) {
  170. process.stdout.write(`\x1B[37;1mUNSUPPORTED\x1B[0m\n`);
  171. counts.unsupported++;
  172. continue;
  173. }
  174. const source = fs.readFileSync(value.path, 'utf-8');
  175. const diff_info = await license_checker.compare({
  176. filename: value.name,
  177. source,
  178. })
  179. if ( ! diff_info ) {
  180. counts.error++;
  181. continue;
  182. }
  183. if ( ! diff_info.has_header ) {
  184. counts.missing++;
  185. process.stdout.write(`\x1B[33;1mMISSING\x1B[0m\n`);
  186. continue;
  187. }
  188. if ( diff_info ) {
  189. if ( diff_info.distance !== 0 ) {
  190. counts.conflict++;
  191. process.stdout.write(`\x1B[31;1mCONFLICT\x1B[0m\n`);
  192. } else {
  193. counts.ok++;
  194. process.stdout.write(`\x1B[32;1mOK\x1B[0m\n`);
  195. }
  196. } else {
  197. console.log('NO COMMENT');
  198. }
  199. }
  200. const { Table } = require('console-table-printer');
  201. const t = new Table({
  202. columns: [
  203. {
  204. title: 'License Header',
  205. name: 'situation', alignment: 'left', color: 'white_bold' },
  206. {
  207. title: 'Number of Files',
  208. name: 'count', alignment: 'right' },
  209. ],
  210. colorMap: {
  211. green: '\x1B[32;1m',
  212. yellow: '\x1B[33;1m',
  213. red: '\x1B[31;1m',
  214. }
  215. });
  216. console.log('');
  217. if ( counts.error > 0 ) {
  218. console.log(`\x1B[31;1mTHERE WERE SOME ERRORS!\x1B[0m`);
  219. console.log('check the log above for the stack trace');
  220. console.log('');
  221. t.addRow({ situation: 'error', count: counts.error },
  222. { color: 'red' });
  223. }
  224. console.log(dedent(`
  225. \x1B[31;1mAny text below is mostly lies!\x1B[0m
  226. This tool is still being developed and most of what's
  227. described is "the plan" rather than a thing that will
  228. actually happen.
  229. \x1B[31;1m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\x1B[0m
  230. `));
  231. if ( counts.conflict ) {
  232. console.log(dedent(`
  233. \x1B[37;1mIt looks like you have some conflicts!\x1B[0m
  234. Run the following command to update license headers:
  235. \x1B[36;1maddlicense sync\x1B[0m
  236. This will begin an interactive license update.
  237. Any time the license doesn't quite match you will
  238. be given the option to replace it or skip the file.
  239. \x1B[90mSee \`addlicense help sync\` for other options.\x1B[0m
  240. You will also be able to choose
  241. "remember for headers matching this one"
  242. if you know the same issue will come up later.
  243. `));
  244. } else if ( counts.missing ) {
  245. console.log(dedent(`
  246. \x1B[37;1mSome missing license headers!\x1B[0m
  247. Run the following command to add the missing license headers:
  248. \x1B[36;1maddlicense sync\x1B[0m
  249. `));
  250. } else {
  251. console.log(dedent(`
  252. \x1B[37;1mNo action to perform!\x1B[0m
  253. Run the following command to do absolutely nothing:
  254. \x1B[36;1maddlicense sync\x1B[0m
  255. `));
  256. }
  257. console.log('');
  258. t.addRow({ situation: 'ok', count: counts.ok },
  259. { color: 'green' });
  260. t.addRow({ situation: 'missing', count: counts.missing },
  261. { color: 'yellow' });
  262. t.addRow({ situation: 'conflict', count: counts.conflict },
  263. { color: 'red' });
  264. t.addRow({ situation: 'unsupported', count: counts.unsupported });
  265. t.printTable();
  266. };
  267. const main = async () => {
  268. const { program } = require('commander');
  269. const helptext = dedent(`
  270. Usage: usage text
  271. `);
  272. const run_command = async ({ cmd, cmd_fn }) => {
  273. const options = {
  274. program: program.opts(),
  275. command: cmd.opts(),
  276. };
  277. console.log('options', options);
  278. if ( ! fs.existsSync(options.program.config) ) {
  279. // TODO: configuration wizard
  280. fs.writeFileSync(options.program.config, '');
  281. }
  282. await cmd_fn({ options });
  283. };
  284. program
  285. .name('addlicense')
  286. .option('-c, --config', 'configuration file', 'addlicense.yml')
  287. .addHelpText('before', helptext)
  288. ;
  289. const cmd_check = program.command('check')
  290. .description('check license headers')
  291. .option('-n, --non-interactive', 'disable prompting')
  292. .action(() => {
  293. run_command({ cmd: cmd_check, cmd_fn: cmd_check_fn });
  294. })
  295. const cmd_sync = program.command('sync')
  296. .description('synchronize files with license header rules')
  297. .option('-n, --non-interactive', 'disable prompting')
  298. .action(() => {
  299. console.log('called sync');
  300. console.log(program.opts());
  301. console.log(cmd_sync.opts());
  302. })
  303. program.parse(process.argv);
  304. };
  305. if ( require.main === module ) {
  306. main();
  307. }