chore: runtime linting (#12314)

- check that the runtime doesn't use methods that are too new
- add linting rule to prevent references to the compiler in the runtime (this is important for the first check, else the ambient node typings would be included, which includes a definition for `at()`, which means we no longer would get errors when violating the "don't use new methods" rule in the runtime)
- fix code as a result of these new checks

closes #10438
pull/12329/head
Simon H 6 months ago committed by GitHub
parent ed6084806d
commit 5eff68f63d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,6 +1,38 @@
import svelte_config from '@sveltejs/eslint-config'; import svelte_config from '@sveltejs/eslint-config';
import lube from 'eslint-plugin-lube'; import lube from 'eslint-plugin-lube';
const no_compiler_imports = {
meta: {
type: /** @type {const} */ ('problem'),
docs: {
description:
'Enforce that there are no imports to the compiler in runtime code. ' +
'This prevent accidental inclusion of the compiler runtime and ' +
"ensures that TypeScript does not pick up more ambient types (for example from Node) that shouldn't be available in the browser."
}
},
create(context) {
return {
Program: () => {
// Do a simple string search because ESLint doesn't provide a way to check JSDoc comments.
// The string search could in theory yield false positives, but in practice it's unlikely.
const text = context.sourceCode.getText();
const idx = Math.max(text.indexOf('../compiler/'), text.indexOf('#compiler'));
if (idx !== -1) {
context.report({
loc: {
start: context.sourceCode.getLocFromIndex(idx),
end: context.sourceCode.getLocFromIndex(idx + 12)
},
message:
'References to compiler code are forbidden in runtime code (both for type and value imports)'
});
}
}
};
}
};
/** @type {import('eslint').Linter.FlatConfig[]} */ /** @type {import('eslint').Linter.FlatConfig[]} */
export default [ export default [
...svelte_config, ...svelte_config,
@ -11,7 +43,8 @@ export default [
} }
}, },
plugins: { plugins: {
lube lube,
custom: { rules: { no_compiler_imports } }
}, },
rules: { rules: {
'@typescript-eslint/await-thenable': 'error', '@typescript-eslint/await-thenable': 'error',
@ -37,6 +70,13 @@ export default [
'no-console': 'off' 'no-console': 'off'
} }
}, },
{
files: ['packages/svelte/src/**/*'],
ignores: ['packages/svelte/src/compiler/**/*'],
rules: {
'custom/no_compiler_imports': 'error'
}
},
{ {
ignores: [ ignores: [
'**/*.d.ts', '**/*.d.ts',

@ -106,7 +106,7 @@
"scripts": { "scripts": {
"build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js", "build": "node scripts/process-messages && rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "node scripts/process-messages && rollup -cw", "dev": "node scripts/process-messages && rollup -cw",
"check": "tsc && cd ./tests/types && tsc", "check": "tsc --project tsconfig.runtime.json && tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch", "check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js", "generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json", "generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",

@ -3,7 +3,7 @@
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js'; import fragment from './state/fragment.js';
import { regex_whitespace } from '../patterns.js'; import { regex_whitespace } from '../patterns.js';
import { reserved } from './utils/names.js'; import { reserved } from '../../../constants.js';
import full_char_code_at from './utils/full_char_code_at.js'; import full_char_code_at from './utils/full_char_code_at.js';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js'; import { create_fragment } from './utils/create.js';

@ -1,6 +1,6 @@
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
/** @import * as Compiler from '#compiler' */ /** @import * as Compiler from '#compiler' */
import { is_void } from '../utils/names.js'; import { is_void } from '../../../../constants.js';
import read_expression from '../read/expression.js'; import read_expression from '../read/expression.js';
import { read_script } from '../read/script.js'; import { read_script } from '../read/script.js';
import read_style from '../read/style.js'; import read_style from '../read/style.js';

@ -1,74 +0,0 @@
export const reserved = [
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield'
];
const void_element_names = [
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr'
];
/** @param {string} name */
export function is_void(name) {
return void_element_names.includes(name) || name.toLowerCase() === '!doctype';
}

@ -8,7 +8,7 @@ import type {
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler'; import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js'; import type { TransformState } from '../types.js';
import type { ComponentAnalysis } from '../../types.js'; import type { ComponentAnalysis } from '../../types.js';
import type { Location } from 'locate-character'; import type { SourceLocation } from '#shared';
export interface ClientTransformState extends TransformState { export interface ClientTransformState extends TransformState {
readonly private_state: Map<string, StateField>; readonly private_state: Map<string, StateField>;
@ -24,10 +24,6 @@ export interface ClientTransformState extends TransformState {
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>; readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
} }
export type SourceLocation =
| [line: number, column: number]
| [line: number, column: number, SourceLocation[]];
export interface ComponentClientTransformState extends ClientTransformState { export interface ComponentClientTransformState extends ClientTransformState {
readonly analysis: ComponentAnalysis; readonly analysis: ComponentAnalysis;
readonly options: ValidatedCompileOptions; readonly options: ValidatedCompileOptions;

@ -1051,7 +1051,7 @@ function serialize_bind_this(bind_this, context, node) {
} }
/** /**
* @param {import('../types.js').SourceLocation[]} locations * @param {import('#shared').SourceLocation[]} locations
*/ */
function serialize_locations(locations) { function serialize_locations(locations) {
return b.array( return b.array(
@ -1905,7 +1905,7 @@ export const template_visitors = {
state.init.push(b.stmt(b.call('$.transition', ...args))); state.init.push(b.stmt(b.call('$.transition', ...args)));
}, },
RegularElement(node, context) { RegularElement(node, context) {
/** @type {import('../types.js').SourceLocation} */ /** @type {import('#shared').SourceLocation} */
let location = [-1, -1]; let location = [-1, -1];
if (context.state.options.dev) { if (context.state.options.dev) {
@ -2133,7 +2133,7 @@ export const template_visitors = {
context.state.template.push('>'); context.state.template.push('>');
/** @type {import('../types.js').SourceLocation[]} */ /** @type {import('#shared').SourceLocation[]} */
const child_locations = []; const child_locations = [];
/** @type {import('../types').ComponentClientTransformState} */ /** @type {import('../types').ComponentClientTransformState} */

@ -293,3 +293,78 @@ export function is_capture_event(name, mode = 'exclude-on') {
? name !== 'gotpointercapture' && name !== 'lostpointercapture' ? name !== 'gotpointercapture' && name !== 'lostpointercapture'
: name !== 'ongotpointercapture' && name !== 'onlostpointercapture'; : name !== 'ongotpointercapture' && name !== 'onlostpointercapture';
} }
export const reserved = [
'arguments',
'await',
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'enum',
'eval',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'implements',
'import',
'in',
'instanceof',
'interface',
'let',
'new',
'null',
'package',
'private',
'protected',
'public',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield'
];
const void_element_names = [
'area',
'base',
'br',
'col',
'command',
'embed',
'hr',
'img',
'input',
'keygen',
'link',
'meta',
'param',
'source',
'track',
'wbr'
];
/** @param {string} name */
export function is_void(name) {
return void_element_names.includes(name) || name.toLowerCase() === '!doctype';
}

@ -1,4 +1,4 @@
/** @import { SourceLocation } from '../../../compiler/phases/3-transform/client/types.js' */ /** @import { SourceLocation } from '#shared' */
import { HYDRATION_END, HYDRATION_START } from '../../../constants.js'; import { HYDRATION_END, HYDRATION_START } from '../../../constants.js';
import { hydrating } from '../dom/hydration.js'; import { hydrating } from '../dom/hydration.js';

@ -224,10 +224,11 @@ function handle_error(error, effect, component_context) {
let current_context = component_context; let current_context = component_context;
while (current_context !== null) { while (current_context !== null) {
/** @type {string} */
var filename = current_context.function?.filename; var filename = current_context.function?.filename;
if (filename) { if (filename) {
const file = filename.split('/').at(-1); const file = filename.split('/').pop();
component_stack.push(file); component_stack.push(file);
} }

@ -2,3 +2,7 @@ export type Store<V> = {
subscribe: (run: (value: V) => void) => () => void; subscribe: (run: (value: V) => void) => () => void;
set(value: V): void; set(value: V): void;
}; };
export type SourceLocation =
| [line: number, column: number]
| [line: number, column: number, SourceLocation[]];

@ -1,4 +1,4 @@
import { is_void } from '../../compiler/phases/1-parse/utils/names.js'; import { is_void } from '../../constants.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';

@ -0,0 +1,15 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
// Ensure we don't use any methods that are not available in all browser for a long period of time
// so that people don't need to polyfill them. Example array.at(...) was introduced to Safari only in 2022
"target": "es2021",
"lib": ["es2021", "DOM", "DOM.Iterable"],
"types": [] // prevent automatic inclusion of @types/node
},
"include": ["./src/"],
// Compiler is allowed to use more recent methods; people using it in the browser are expected to know
// how to polyfill missing methods. Also make sure to not include test files as these include Vitest
// which then loads node globals.
"exclude": ["./src/compiler/**/*", "./src/**/*.test.ts"]
}
Loading…
Cancel
Save