main.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. /*
  2. * Copyright (C) 2024 Puter Technologies Inc.
  3. *
  4. * This file is part of Puter.
  5. *
  6. * Puter is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. const levenshtein = require('js-levenshtein');
  20. const DiffMatchPatch = require('diff-match-patch');
  21. const dmp = new DiffMatchPatch();
  22. const dedent = require('dedent');
  23. const { walk, EXCLUDE_LISTS } = require('file-walker');
  24. const { CommentParser } = require('../comment-parser/main');
  25. const fs = require('fs');
  26. const path_ = require('path');
  27. const CompareFn = ({ header1, header2, distance_only = false }) => {
  28. // Calculate Levenshtein distance
  29. const distance = levenshtein(header1, header2);
  30. // console.log(`Levenshtein distance: ${distance}`);
  31. if ( distance_only ) return { distance };
  32. // Generate diffs using diff-match-patch
  33. const diffs = dmp.diff_main(header1, header2);
  34. dmp.diff_cleanupSemantic(diffs);
  35. let term_diff = '';
  36. // Manually format diffs for terminal display
  37. diffs.forEach(([type, text]) => {
  38. switch (type) {
  39. case DiffMatchPatch.DIFF_INSERT:
  40. term_diff += `\x1b[32m${text}\x1b[0m`; // Green for insertions
  41. break;
  42. case DiffMatchPatch.DIFF_DELETE:
  43. term_diff += `\x1b[31m${text}\x1b[0m`; // Red for deletions
  44. break;
  45. case DiffMatchPatch.DIFF_EQUAL:
  46. term_diff += text; // No color for equal parts
  47. break;
  48. }
  49. });
  50. return {
  51. distance,
  52. term_diff,
  53. };
  54. }
  55. const LicenseChecker = ({
  56. comment_parser,
  57. desired_header,
  58. }) => {
  59. const supports = ({ filename }) => {
  60. return comment_parser.supports({ filename });
  61. };
  62. const compare = async ({ filename, source }) => {
  63. const headers = await comment_parser.extract_top_comments(
  64. { filename, source });
  65. const headers_lines = headers.map(h => h.lines);
  66. if ( headers.length < 1 ) {
  67. return {
  68. has_header: false,
  69. };
  70. }
  71. // console.log('headers', headers);
  72. let top = 0;
  73. let bottom = 0;
  74. let current_distance = Number.MAX_SAFE_INTEGER;
  75. // "wah"
  76. for ( let i=1 ; i <= headers.length ; i++ ) {
  77. const combined = headers_lines.slice(top, i).flat();
  78. const combined_txt = combined.join('\n');
  79. const { distance } =
  80. CompareFn({
  81. header1: desired_header,
  82. header2: combined_txt,
  83. distance_only: true,
  84. });
  85. if ( distance < current_distance ) {
  86. current_distance = distance;
  87. bottom = i;
  88. } else {
  89. break;
  90. }
  91. }
  92. // "woop"
  93. for ( let i=1 ; i < headers.length ; i++ ) {
  94. const combined = headers_lines.slice(i, bottom).flat();
  95. const combined_txt = combined.join('\n');
  96. const { distance } =
  97. CompareFn({
  98. header1: desired_header,
  99. header2: combined_txt,
  100. distance_only: true,
  101. });
  102. if ( distance < current_distance ) {
  103. current_distance = distance;
  104. top = i;
  105. } else {
  106. break;
  107. }
  108. }
  109. const combined = headers_lines.slice(top, bottom).flat();
  110. const combined_txt = combined.join('\n');
  111. const diff_info = CompareFn({
  112. header1: desired_header,
  113. header2: combined_txt,
  114. })
  115. diff_info.range = [
  116. headers[top].range[0],
  117. headers[bottom-1].range[1],
  118. ];
  119. diff_info.has_header = true;
  120. return diff_info;
  121. };
  122. return {
  123. compare,
  124. supports,
  125. };
  126. };
  127. const license_check_test = async ({ options }) => {
  128. const comment_parser = CommentParser();
  129. const license_checker = LicenseChecker({
  130. comment_parser,
  131. desired_header: fs.readFileSync(
  132. path_.join(__dirname, '../../doc/license_header.txt'),
  133. 'utf-8',
  134. ),
  135. });
  136. const walk_iterator = walk({
  137. excludes: EXCLUDE_LISTS.NOT_SOURCE,
  138. }, path_.join(__dirname, '../..'));
  139. for await ( const value of walk_iterator ) {
  140. if ( value.is_dir ) continue;
  141. if ( value.name !== 'dev-console-ui-utils.js' ) continue;
  142. console.log(value.path);
  143. const source = fs.readFileSync(value.path, 'utf-8');
  144. const diff_info = await license_checker.compare({
  145. filename: value.name,
  146. source,
  147. })
  148. if ( diff_info ) {
  149. process.stdout.write('\x1B[36;1m=======\x1B[0m\n');
  150. process.stdout.write(diff_info.term_diff);
  151. process.stdout.write('\n\x1B[36;1m=======\x1B[0m\n');
  152. // console.log('headers', headers);
  153. } else {
  154. console.log('NO COMMENT');
  155. }
  156. console.log('RANGE', diff_info.range)
  157. const new_comment = comment_parser.output_comment({
  158. filename: value.name,
  159. style: 'block',
  160. text: 'some text\nto display'
  161. });
  162. console.log('NEW COMMENT?', new_comment);
  163. }
  164. };
  165. const cmd_check_fn = async () => {
  166. const comment_parser = CommentParser();
  167. const license_checker = LicenseChecker({
  168. comment_parser,
  169. desired_header: fs.readFileSync(
  170. path_.join(__dirname, '../../doc/license_header.txt'),
  171. 'utf-8',
  172. ),
  173. });
  174. const counts = {
  175. ok: 0,
  176. missing: 0,
  177. conflict: 0,
  178. error: 0,
  179. unsupported: 0,
  180. };
  181. const walk_iterator = walk({
  182. excludes: EXCLUDE_LISTS.NOT_SOURCE,
  183. }, path_.join(__dirname, '../..'));
  184. for await ( const value of walk_iterator ) {
  185. if ( value.is_dir ) continue;
  186. process.stdout.write(value.path + ' ... ');
  187. if ( ! license_checker.supports({ filename: value.name }) ) {
  188. process.stdout.write(`\x1B[37;1mUNSUPPORTED\x1B[0m\n`);
  189. counts.unsupported++;
  190. continue;
  191. }
  192. const source = fs.readFileSync(value.path, 'utf-8');
  193. const diff_info = await license_checker.compare({
  194. filename: value.name,
  195. source,
  196. })
  197. if ( ! diff_info ) {
  198. counts.error++;
  199. continue;
  200. }
  201. if ( ! diff_info.has_header ) {
  202. counts.missing++;
  203. process.stdout.write(`\x1B[33;1mMISSING\x1B[0m\n`);
  204. continue;
  205. }
  206. if ( diff_info ) {
  207. if ( diff_info.distance !== 0 ) {
  208. counts.conflict++;
  209. process.stdout.write(`\x1B[31;1mCONFLICT\x1B[0m\n`);
  210. } else {
  211. counts.ok++;
  212. process.stdout.write(`\x1B[32;1mOK\x1B[0m\n`);
  213. }
  214. } else {
  215. console.log('NO COMMENT');
  216. }
  217. }
  218. const { Table } = require('console-table-printer');
  219. const t = new Table({
  220. columns: [
  221. {
  222. title: 'License Header',
  223. name: 'situation', alignment: 'left', color: 'white_bold' },
  224. {
  225. title: 'Number of Files',
  226. name: 'count', alignment: 'right' },
  227. ],
  228. colorMap: {
  229. green: '\x1B[32;1m',
  230. yellow: '\x1B[33;1m',
  231. red: '\x1B[31;1m',
  232. }
  233. });
  234. console.log('');
  235. if ( counts.error > 0 ) {
  236. console.log(`\x1B[31;1mTHERE WERE SOME ERRORS!\x1B[0m`);
  237. console.log('check the log above for the stack trace');
  238. console.log('');
  239. t.addRow({ situation: 'error', count: counts.error },
  240. { color: 'red' });
  241. }
  242. console.log(dedent(`
  243. \x1B[31;1mAny text below is mostly lies!\x1B[0m
  244. This tool is still being developed and most of what's
  245. described is "the plan" rather than a thing that will
  246. actually happen.
  247. \x1B[31;1m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\x1B[0m
  248. `));
  249. if ( counts.conflict ) {
  250. console.log(dedent(`
  251. \x1B[37;1mIt looks like you have some conflicts!\x1B[0m
  252. Run the following command to update license headers:
  253. \x1B[36;1maddlicense sync\x1B[0m
  254. This will begin an interactive license update.
  255. Any time the license doesn't quite match you will
  256. be given the option to replace it or skip the file.
  257. \x1B[90mSee \`addlicense help sync\` for other options.\x1B[0m
  258. You will also be able to choose
  259. "remember for headers matching this one"
  260. if you know the same issue will come up later.
  261. `));
  262. } else if ( counts.missing ) {
  263. console.log(dedent(`
  264. \x1B[37;1mSome missing license headers!\x1B[0m
  265. Run the following command to add the missing license headers:
  266. \x1B[36;1maddlicense sync\x1B[0m
  267. `));
  268. } else {
  269. console.log(dedent(`
  270. \x1B[37;1mNo action to perform!\x1B[0m
  271. Run the following command to do absolutely nothing:
  272. \x1B[36;1maddlicense sync\x1B[0m
  273. `));
  274. }
  275. console.log('');
  276. t.addRow({ situation: 'ok', count: counts.ok },
  277. { color: 'green' });
  278. t.addRow({ situation: 'missing', count: counts.missing },
  279. { color: 'yellow' });
  280. t.addRow({ situation: 'conflict', count: counts.conflict },
  281. { color: 'red' });
  282. t.addRow({ situation: 'unsupported', count: counts.unsupported });
  283. t.printTable();
  284. };
  285. const cmd_sync_fn = async () => {
  286. const comment_parser = CommentParser();
  287. const desired_header = fs.readFileSync(
  288. path_.join(__dirname, '../../doc/license_header.txt'),
  289. 'utf-8',
  290. );
  291. const license_checker = LicenseChecker({
  292. comment_parser,
  293. desired_header,
  294. });
  295. const counts = {
  296. ok: 0,
  297. missing: 0,
  298. conflict: 0,
  299. error: 0,
  300. unsupported: 0,
  301. };
  302. const walk_iterator = walk({
  303. excludes: EXCLUDE_LISTS.NOT_SOURCE,
  304. }, '.');
  305. for await ( const value of walk_iterator ) {
  306. if ( value.is_dir ) continue;
  307. process.stdout.write(value.path + ' ... ');
  308. if ( ! license_checker.supports({ filename: value.name }) ) {
  309. process.stdout.write(`\x1B[37;1mUNSUPPORTED\x1B[0m\n`);
  310. counts.unsupported++;
  311. continue;
  312. }
  313. const source = fs.readFileSync(value.path, 'utf-8');
  314. const diff_info = await license_checker.compare({
  315. filename: value.name,
  316. source,
  317. })
  318. if ( ! diff_info ) {
  319. counts.error++;
  320. continue;
  321. }
  322. if ( ! diff_info.has_header ) {
  323. if ( false ) fs.writeFileSync(
  324. value.path,
  325. comment_parser.output_comment({
  326. style: 'block',
  327. filename: value.name,
  328. text: desired_header,
  329. }) +
  330. '\n' +
  331. source
  332. );
  333. continue;
  334. }
  335. if ( diff_info ) {
  336. if ( diff_info.distance !== 0 ) {
  337. counts.conflict++;
  338. process.stdout.write(`\x1B[31;1mCONFLICT\x1B[0m\n`);
  339. } else {
  340. counts.ok++;
  341. process.stdout.write(`\x1B[32;1mOK\x1B[0m\n`);
  342. }
  343. } else {
  344. console.log('NO COMMENT');
  345. }
  346. }
  347. const { Table } = require('console-table-printer');
  348. const t = new Table({
  349. columns: [
  350. {
  351. title: 'License Header',
  352. name: 'situation', alignment: 'left', color: 'white_bold' },
  353. {
  354. title: 'Number of Files',
  355. name: 'count', alignment: 'right' },
  356. ],
  357. colorMap: {
  358. green: '\x1B[32;1m',
  359. yellow: '\x1B[33;1m',
  360. red: '\x1B[31;1m',
  361. }
  362. });
  363. console.log('');
  364. if ( counts.error > 0 ) {
  365. console.log(`\x1B[31;1mTHERE WERE SOME ERRORS!\x1B[0m`);
  366. console.log('check the log above for the stack trace');
  367. console.log('');
  368. t.addRow({ situation: 'error', count: counts.error },
  369. { color: 'red' });
  370. }
  371. console.log(dedent(`
  372. \x1B[31;1mAny text below is mostly lies!\x1B[0m
  373. This tool is still being developed and most of what's
  374. described is "the plan" rather than a thing that will
  375. actually happen.
  376. \x1B[31;1m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\x1B[0m
  377. `));
  378. if ( counts.conflict ) {
  379. console.log(dedent(`
  380. \x1B[37;1mIt looks like you have some conflicts!\x1B[0m
  381. Run the following command to update license headers:
  382. \x1B[36;1maddlicense sync\x1B[0m
  383. This will begin an interactive license update.
  384. Any time the license doesn't quite match you will
  385. be given the option to replace it or skip the file.
  386. \x1B[90mSee \`addlicense help sync\` for other options.\x1B[0m
  387. You will also be able to choose
  388. "remember for headers matching this one"
  389. if you know the same issue will come up later.
  390. `));
  391. } else if ( counts.missing ) {
  392. console.log(dedent(`
  393. \x1B[37;1mSome missing license headers!\x1B[0m
  394. Run the following command to add the missing license headers:
  395. \x1B[36;1maddlicense sync\x1B[0m
  396. `));
  397. } else {
  398. console.log(dedent(`
  399. \x1B[37;1mNo action to perform!\x1B[0m
  400. Run the following command to do absolutely nothing:
  401. \x1B[36;1maddlicense sync\x1B[0m
  402. `));
  403. }
  404. console.log('');
  405. t.addRow({ situation: 'ok', count: counts.ok },
  406. { color: 'green' });
  407. t.addRow({ situation: 'missing', count: counts.missing },
  408. { color: 'yellow' });
  409. t.addRow({ situation: 'conflict', count: counts.conflict },
  410. { color: 'red' });
  411. t.addRow({ situation: 'unsupported', count: counts.unsupported });
  412. t.printTable();
  413. };
  414. const main = async () => {
  415. const { program } = require('commander');
  416. const helptext = dedent(`
  417. Usage: usage text
  418. `);
  419. const run_command = async ({ cmd, cmd_fn }) => {
  420. const options = {
  421. program: program.opts(),
  422. command: cmd.opts(),
  423. };
  424. console.log('options', options);
  425. if ( ! fs.existsSync(options.program.config) ) {
  426. // TODO: configuration wizard
  427. fs.writeFileSync(options.program.config, '');
  428. }
  429. await cmd_fn({ options });
  430. };
  431. program
  432. .name('addlicense')
  433. .option('-c, --config', 'configuration file', 'addlicense.yml')
  434. .addHelpText('before', helptext)
  435. ;
  436. const cmd_check = program.command('check')
  437. .description('check license headers')
  438. .option('-n, --non-interactive', 'disable prompting')
  439. .action(() => {
  440. run_command({ cmd: cmd_check, cmd_fn: cmd_check_fn });
  441. })
  442. const cmd_sync = program.command('sync')
  443. .description('synchronize files with license header rules')
  444. .option('-n, --non-interactive', 'disable prompting')
  445. .action(() => {
  446. run_command({ cmd: cmd_sync, cmd_fn: cmd_sync_fn })
  447. })
  448. program.parse(process.argv);
  449. };
  450. if ( require.main === module ) {
  451. main();
  452. }