Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
decatoncheir authored Dec 24, 2024
2 parents 9ca0b07 + 368d01b commit 8c69fdb
Show file tree
Hide file tree
Showing 130 changed files with 16,596 additions and 6,045 deletions.
136 changes: 2 additions & 134 deletions .ci/eslint-plugin-zotero-translator/bin/teslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,140 +2,8 @@

'use strict';

const fs = require('fs');
const path = require('path');
const find = require('recursive-readdir');
const { ESLint } = require("eslint");
const argv = require('commander');

const translators = require('../lib/translators');
process.argv = process.argv.map(arg => arg === '--output-json' ? [ '--format', 'json', '--output-file' ] : arg).flat();

argv
.version(ESLint.version)
.option('-o, --output-json [file | -]', 'Write report to file or stdout as JSON')
.option('-f, --fix', 'Automatically fix problems')
.option('--no-ignore', 'Disable use of ignore files and patterns')
.option('--quiet', 'Report errors only - default: false')
.option('--dump-decorated [file]', 'Dump decorated translator to file for inspection')
.parse(process.argv);

/* PATCHES */
// disable the processor so that fixing works
const eslintPluginZoteroTranslator = require('eslint-plugin-zotero-translator');
delete eslintPluginZoteroTranslator.processors;

async function main() {
// split sources to lint into regular javascript (handled by lintFiles) and translators (handled by lintText)
const sources = {
javascripts: [],
translators: [],
errors: 0,
};

let allResults = [];

function findIgnore(file, stats) {
if (stats.isDirectory()) return (path.basename(file) == "node_modules" || path.basename(file) == ".ci");
return !file.endsWith('.js');
}
for (const target of argv.args) {
if (!fs.existsSync(target)) {
console.error(`Target file '${target}' does not exist; skipping`); // eslint-disable-line no-console
continue;
}
const files = fs.lstatSync(target).isDirectory() ? await find(target, [findIgnore]) : [target];
for (const file of files) {
if (path.dirname(path.resolve(file)) === translators.cache.repo) {
const translator = translators.cache.get(file);
if (translator.header) {
translator.filename = file;
sources.translators.push(translator);
}
else {
sources.javascripts.push(file);
}
}
else {
sources.javascripts.push(file);
}
}
}

const eslint = new ESLint({
cwd: translators.cache.repo,
fix: argv.fix,
ignore: !!argv.ignore, // otherwise you can't lint stuff in hidden dirs
});
const formatter = await eslint.loadFormatter();
function showResults(files, results) {
if (argv.quiet) results = ESLint.getErrorResults(results);
for (const res of results) {
sources.errors += res.errorCount;
}

if (results.length) {
console.log(formatter.format(results)); // eslint-disable-line no-console
}
else {
if (Array.isArray(files)) files = files.join(', ');
if (!argv.quiet) console.log(files, 'OK'); // eslint-disable-line no-console
}
}

if (sources.javascripts.length) {
const results = await eslint.lintFiles(sources.javascripts);
if (argv.fix) {
for (const result of results) {
if (result.messages.find(msg => msg.ruleId === 'notice/notice' && msg.fix)) {
console.log(`Not safe to apply 'notice/notice' to ${result.filePath}`); // eslint-disable-line no-console
process.exit(1); // eslint-disable-line no-process-exit
}
}
ESLint.outputFixes(results);
}
if (argv.outputJson) {
allResults.push(...results);
}
else {
showResults(sources.javascripts, results);
}
}

for (const translator of sources.translators) {
if (argv.dumpDecorated) fs.writeFileSync(argv.dumpDecorated, translator.source, 'utf-8');
const results = await eslint.lintText(translator.source, { filePath: translator.filename });
if (argv.fix) {
for (const result of results) {
if (result.output) {
try {
fs.writeFileSync(result.filePath, translators.strip(result.output), 'utf-8');
}
catch (err) {
console.log(`Error writing fixed ${result.filePath}: ${err.message}`); // eslint-disable-line no-console
process.exit(1); // eslint-disable-line no-process-exit
}
}
}
}
if (argv.outputJson) {
allResults.push(...results);
}
else {
showResults(translator.filename, results);
}
}

if (argv.outputJson) {
if (argv.outputJson === '-') {
process.stdout.write(JSON.stringify(allResults) + '\n');
}
else {
fs.writeFileSync(argv.outputJson, JSON.stringify(allResults), 'utf-8');
}
}
else {
process.exit(sources.errors); // eslint-disable-line no-process-exit
}
}

main();
require('../../../node_modules/.bin/eslint')
4 changes: 2 additions & 2 deletions .ci/eslint-plugin-zotero-translator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ const requireDir = require('require-dir');
module.exports = {
rules: requireDir('./lib/rules'),
processors: {
'.js': require('./lib/processor'),
}
translator: require('./processor'),
},
};
15 changes: 0 additions & 15 deletions .ci/eslint-plugin-zotero-translator/lib/processor/index.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
'use strict';

const { parsed, header } = require('../../processor').support;

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce valid lastUpdated in header',
category: 'Possible Errors',
},
fixable: 'code',
},

create: function (context) {
return {
Program: function (node) {
const filename = context.getFilename();
const translator = parsed(filename);
if (!translator || !translator.header.fields) return; // regular js source, or header is invalid

const lastUpdated = header(node).properties.find(p => p.key.value === 'lastUpdated');

if (!lastUpdated) {
context.report({
loc: { start: { line: 1, column: 1 } },
message: 'Header needs lastUpdated field',
});
return;
}

const format = date => date.toISOString().replace('T', ' ').replace(/\..*/, '');
const now = format(new Date());
const fix = fixer => fixer.replaceText(lastUpdated.value, `"${now}"`);

if (typeof lastUpdated.value.value !== 'string' || !lastUpdated.value.value.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) {
context.report({
node: lastUpdated.value,
message: `lastUpdated field must be a string in YYYY-MM-DD HH:MM:SS format`,
fix,
});
return;
}

if (translator.lastUpdated && translator.lastUpdated > lastUpdated.value.value) {
context.report({
node: lastUpdated.value,
message: `lastUpdated field must be updated to be > ${translator.lastUpdated} to push to clients`,
fix,
});
}
}
};
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict';

const fs = require('fs');
const path = require('path');
const uuid = require('uuid/v4');

const { repo, parsed, header, IDconflict } = require('../../processor').support;

const deleted = new Set(
fs.readFileSync(path.join(repo, 'deleted.txt'), 'utf-8')
.split('\n')
.map(line => line.split(' ')[0])
.filter(id => id && id.indexOf('-') > 0)
);

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'disallows translatorID re-use',
category: 'Potential Problems',
},
fixable: 'code',
},

create: function (context) {
return {
Program: function (node) {
const filename = context.getFilename();
const translator = parsed(filename);
if (!translator || !translator.header.fields) return; // regular js source, or header is invalid

const translatorID = header(node).properties.find(p => p.key.value === 'translatorID');

if (!translatorID || !translatorID.value.value) {
context.report({
node: header(node),
message: 'Header has no translator ID',
});
return;
}

if (deleted.has(translatorID.value.value)) {
context.report({
node: translatorID.value,
message: 'Header re-uses translator ID of deleted translator',
fix: function (fixer) {
return fixer.replaceText(translatorID.value, `"${uuid()}"`);
}
});
return;
}

const conflict = IDconflict(filename);
if (conflict) {
context.report({
node: translatorID.value,
message: `re-uses translator ID of ${conflict.label}`,
fix: fixer => fixer.replaceText(translatorID.value, `"${uuid()}"`),
});
}
}
};
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
'use strict';

const { parsed } = require('../../processor').support;

module.exports = {
meta: {
type: 'problem',
docs: {
description: 'enforce translatorType against handler functions',
category: 'Possible Errors',
},
},

create: function (context) {
return {
Program: function (program) {
const translator = parsed(context.getFilename());
if (!translator || !translator.header.fields) return; // regular js source, or header is invalid

const functions = program.body.map((node) => {
if (node.type === 'FunctionDeclaration') return node.id && node.id.name;
if (node.type === 'VariableDeclaration'
&& node.declarations.length === 1
&& node.declarations[0].init
&& node.declarations[0].init.type === 'FunctionExpression') {
return node.declarations[0].id.name;
}
return null;
})
.filter(name => name);

const type = {
import: 1,
export: 2,
web: 4,
search: 8
};

const translatorType = translator.header.fields.translatorType;
const browserSupport = translator.header.fields.browserSupport;

if (browserSupport && !(translatorType & type.web)) {
context.report({
loc: { start: { line: 1, column: 1 } },
message: `browserSupport set, but translatorType (${translatorType}) does not include web (${type.web})`,
});
return;
}

for (const name of ['detectWeb', 'doWeb', 'detectImport', 'doImport', 'doExport', 'detectSearch', 'doSearch']) {
const handler = functions.includes(name);
const mode = name.replace(/^(detect|do)/, '').toLowerCase();
const bit = type[mode];
if (handler && !(translatorType & bit)) {
context.report({
loc: { start: { line: 1, column: 1 } },
message: `${name} present, but translatorType (${translatorType}) does not specify ${mode} (${bit})`,
});
return;
}
if (!handler && (translatorType & bit)) {
let message = `translatorType specifies ${mode} (${bit}), but no ${name} present`;
if (translatorType & type.web && mode !== 'web') {
// Lots of common errors involve web translator developers not understanding
// translator type jargon and checking too many boxes - checking "search"
// because the translator supports search pages, or "import" because it
// imports items from the site.
// Be extra explicit when it seems like that might be the situation.
message += `. This web translator is probably NOT a${bit <= 2 ? 'n' : ''} ${mode} translator, `
+ `even if it supports "${mode}" pages or "${mode}ing". Uncheck "${mode}" in Scaffold.`;
}
context.report({
loc: { start: { line: 1, column: 1 } },
message,
});
return;
}
}
}
};
},
};
Loading

0 comments on commit 8c69fdb

Please sign in to comment.