function analyze_component(root, source, options) {
	const scope_root = new ScopeRoot();

	const module = js(root.module, scope_root, false, null);
	const instance = js(root.instance, scope_root, true, module.scope);

	const { scope, scopes } = create_scopes(root.fragment, scope_root, false, instance.scope);

	/** @type {Template} */
	const template = { ast: root.fragment, scope, scopes };

	let synthetic_stores_legacy_check = [];

	// create synthetic bindings for store subscriptions
	for (const [name, references] of module.scope.references) {
		if (name[0] !== '$' || RESERVED.includes(name)) continue;
		if (name === '$' || name[1] === '$') {
			e.global_reference_invalid(references[0].node, name);
		}

		const store_name = name.slice(1);
		const declaration = instance.scope.get(store_name);
		const init = /** @type {Node | undefined} */ (declaration?.initial);

		// If we're not in legacy mode through the compiler option, assume the user
		// is referencing a rune and not a global store.
		if (
			options.runes === false ||
			!is_rune(name) ||
			(declaration !== null &&
				// const state = $state(0) is valid
				(get_rune(init, instance.scope) === null ||
					// rune-line names received as props are valid too (but we have to protect against $props as store)
					(store_name !== 'props' && get_rune(init, instance.scope) === '$props')) &&
				// allow `import { derived } from 'svelte/store'` in the same file as `const x = $derived(..)` because one is not a subscription to the other
				!(
					name === '$derived' &&
					declaration.initial?.type === 'ImportDeclaration' &&
					declaration.initial.source.value === 'svelte/store'
				))
		) {
			let is_nested_store_subscription_node = undefined;
			search: for (const reference of references) {
				for (let i = reference.path.length - 1; i >= 0; i--) {
					const scope =
						scopes.get(reference.path[i]) ||
						module.scopes.get(reference.path[i]) ||
						instance.scopes.get(reference.path[i]);
					if (scope) {
						const owner = scope?.owner(store_name);
						if (!!owner && owner !== module.scope && owner !== instance.scope) {
							is_nested_store_subscription_node = reference.node;
							break search;
						}
						break;
					}
				}
			}

			if (is_nested_store_subscription_node) {
				e.store_invalid_scoped_subscription(is_nested_store_subscription_node);
			}

			if (options.runes !== false) {
				if (declaration === null && /[a-z]/.test(store_name[0])) {
					e.global_reference_invalid(references[0].node, name);
				} else if (declaration !== null && is_rune(name)) {
					for (const { node, path } of references) {
						if (path.at(-1)?.type === 'CallExpression') {
							w.store_rune_conflict(node, store_name);
						}
					}
				}
			}

			if (module.ast) {
				for (const { node, path } of references) {
					// if the reference is inside module, error. this is a bit hacky but it works
					if (
						/** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) &&
						/** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) &&
						// const state = $state(0) is valid
						get_rune(/** @type {Node} */ (path.at(-1)), module.scope) === null
					) {
						e.store_invalid_subscription(node);
					}
				}
			}

			// we push to the array because at this moment in time we can't be sure if we are in legacy
			// mode yet because we are still changing the module scope
			synthetic_stores_legacy_check.push(() => {
				// if we are creating a synthetic binding for a let declaration we should also declare
				// the declaration as state in case it's reassigned and we are not in runes mode (the function will
				// not be called if we are not in runes mode, that's why there's no !runes check here)
				if (
					declaration !== null &&
					declaration.kind === 'normal' &&
					declaration.declaration_kind === 'let' &&
					declaration.reassigned
				) {
					declaration.kind = 'state';
				}
			});

			const binding = instance.scope.declare(b.id(name), 'store_sub', 'synthetic');
			binding.references = references;
			instance.scope.references.set(name, references);
			module.scope.references.delete(name);
		}
	}

	const component_name = get_component_name(options.filename);

	const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune);

	if (!runes) {
		for (let check of synthetic_stores_legacy_check) {
			check();
		}
	}

	if (runes && root.module) {
		const context = root.module.attributes.find((attribute) => attribute.name === 'context');
		if (context) {
			w.script_context_deprecated(context);
		}
	}

	// TODO remove all the ?? stuff, we don't need it now that we're validating the config
	/** @type {ComponentAnalysis} */
	const analysis = {
		name: module.scope.generate(options.name ?? component_name),
		root: scope_root,
		module,
		instance,
		template,
		elements: [],
		runes,
		tracing: false,
		classes: new Map(),
		immutable: runes || options.immutable,
		exports: [],
		uses_props: false,
		props_id: null,
		uses_rest_props: false,
		uses_slots: false,
		uses_component_bindings: false,
		uses_render_tags: false,
		needs_context: false,
		needs_mutation_validation: false,
		needs_props: false,
		event_directive_node: null,
		uses_event_attributes: false,
		custom_element: options.customElementOptions ?? options.customElement,
		inject_styles: options.css === 'injected' || options.customElement,
		accessors: options.customElement
			? true
			: (runes ? false : !!options.accessors) ||
				// because $set method needs accessors
				options.compatibility?.componentApi === 4,
		reactive_statements: new Map(),
		binding_groups: new Map(),
		slot_names: new Map(),
		css: {
			ast: root.css,
			hash: root.css
				? options.cssHash({
						css: root.css.content.styles,
						filename: options.filename,
						name: component_name,
						hash
					})
				: '',
			keyframes: [],
			has_global: false
		},
		source,
		undefined_exports: new Map(),
		snippet_renderers: new Map(),
		snippets: new Set()
	};

	if (!runes) {
		// every exported `let` or `var` declaration becomes a prop, everything else becomes an export
		for (const node of instance.ast.body) {
			if (node.type !== 'ExportNamedDeclaration') continue;

			analysis.needs_props = true;

			if (node.declaration) {
				if (
					node.declaration.type === 'FunctionDeclaration' ||
					node.declaration.type === 'ClassDeclaration'
				) {
					analysis.exports.push({
						name: /** @type {import('estree').Identifier} */ (node.declaration.id).name,
						alias: null
					});
				} else if (node.declaration.type === 'VariableDeclaration') {
					if (node.declaration.kind === 'const') {
						for (const declarator of node.declaration.declarations) {
							for (const node of extract_identifiers(declarator.id)) {
								analysis.exports.push({ name: node.name, alias: null });
							}
						}
					} else {
						for (const declarator of node.declaration.declarations) {
							for (const id of extract_identifiers(declarator.id)) {
								const binding = /** @type {Binding} */ (instance.scope.get(id.name));
								binding.kind = 'bindable_prop';
							}
						}
					}
				}
			} else {
				for (const specifier of node.specifiers) {
					if (specifier.local.type !== 'Identifier' || specifier.exported.type !== 'Identifier') {
						continue;
					}

					const binding = instance.scope.get(specifier.local.name);

					if (
						binding &&
						(binding.declaration_kind === 'var' || binding.declaration_kind === 'let')
					) {
						binding.kind = 'bindable_prop';

						if (specifier.exported.name !== specifier.local.name) {
							binding.prop_alias = specifier.exported.name;
						}
					} else {
						analysis.exports.push({ name: specifier.local.name, alias: specifier.exported.name });
					}
				}
			}
		}

		// if reassigned/mutated bindings are referenced in `$:` blocks
		// or the template, turn them into state
		for (const binding of instance.scope.declarations.values()) {
			if (binding.kind !== 'normal') continue;

			for (const { node, path } of binding.references) {
				if (node === binding.node) continue;

				if (binding.updated) {
					if (
						path[path.length - 1].type === 'StyleDirective' ||
						path.some((node) => node.type === 'Fragment') ||
						(path[1].type === 'LabeledStatement' && path[1].label.name === '$')
					) {
						binding.kind = 'state';
					}
				}
			}
		}

		// more legacy nonsense: if an `each` binding is reassigned/mutated,
		// treat the expression as being mutated as well
		walk(/** @type {AST.SvelteNode} */ (template.ast), null, {
			EachBlock(node) {
				const scope = /** @type {Scope} */ (template.scopes.get(node));

				for (const binding of scope.declarations.values()) {
					if (binding.updated) {
						const state = { scope: /** @type {Scope} */ (scope.parent), scopes: template.scopes };

						walk(node.expression, state, {
							// @ts-expect-error
							_: set_scope,
							Identifier(node, context) {
								const parent = /** @type {Expression} */ (context.path.at(-1));

								if (is_reference(node, parent)) {
									const binding = context.state.scope.get(node.name);

									if (
										binding &&
										binding.kind === 'normal' &&
										binding.declaration_kind !== 'import'
									) {
										binding.kind = 'state';
										binding.mutated = true;
									}
								}
							}
						});

						break;
					}
				}
			}
		});
	}

	if (root.options) {
		for (const attribute of root.options.attributes) {
			if (attribute.name === 'accessors' && analysis.runes) {
				w.options_deprecated_accessors(attribute);
			}

			if (attribute.name === 'customElement' && !options.customElement) {
				w.options_missing_custom_element(attribute);
			}

			if (attribute.name === 'immutable' && analysis.runes) {
				w.options_deprecated_immutable(attribute);
			}
		}
	}

	if (analysis.runes) {
		const props_refs = module.scope.references.get('$$props');
		if (props_refs) {
			e.legacy_props_invalid(props_refs[0].node);
		}

		const rest_props_refs = module.scope.references.get('$$restProps');
		if (rest_props_refs) {
			e.legacy_rest_props_invalid(rest_props_refs[0].node);
		}

		for (const { ast, scope, scopes } of [module, instance, template]) {
			/** @type {AnalysisState} */
			const state = {
				scope,
				scopes,
				analysis,
				options,
				ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
				parent_element: null,
				has_props_rune: false,
				component_slots: new Set(),
				expression: null,
				state_fields: new Map(),
				function_depth: scope.function_depth,
				reactive_statement: null
			};

			walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
		}

		// warn on any nonstate declarations that are a) reassigned and b) referenced in the template
		for (const scope of [module.scope, instance.scope]) {
			outer: for (const [name, binding] of scope.declarations) {
				if (binding.kind === 'normal' && binding.reassigned) {
					inner: for (const { path } of binding.references) {
						if (path[0].type !== 'Fragment') continue;
						for (let i = 1; i < path.length; i += 1) {
							const type = path[i].type;
							if (
								type === 'FunctionDeclaration' ||
								type === 'FunctionExpression' ||
								type === 'ArrowFunctionExpression'
							) {
								continue inner;
							}
							// bind:this doesn't need to be a state reference if it will never change
							if (
								type === 'BindDirective' &&
								/** @type {AST.BindDirective} */ (path[i]).name === 'this'
							) {
								for (let j = i - 1; j >= 0; j -= 1) {
									const type = path[j].type;
									if (
										type === 'IfBlock' ||
										type === 'EachBlock' ||
										type === 'AwaitBlock' ||
										type === 'KeyBlock'
									) {
										w.non_reactive_update(binding.node, name);
										continue outer;
									}
								}
								continue inner;
							}
						}

						w.non_reactive_update(binding.node, name);
						continue outer;
					}
				}
			}
		}
	} else {
		instance.scope.declare(b.id('$$props'), 'rest_prop', 'synthetic');
		instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic');

		for (const { ast, scope, scopes } of [module, instance, template]) {
			/** @type {AnalysisState} */
			const state = {
				scope,
				scopes,
				analysis,
				options,
				parent_element: null,
				has_props_rune: false,
				ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
				reactive_statement: null,
				component_slots: new Set(),
				expression: null,
				state_fields: new Map(),
				function_depth: scope.function_depth
			};

			walk(/** @type {AST.SvelteNode} */ (ast), state, visitors);
		}

		for (const [name, binding] of instance.scope.declarations) {
			if (
				(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
				binding.node.name !== '$$props'
			) {
				const references = binding.references.filter(
					(r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier'
				);
				if (!references.length && !instance.scope.declarations.has(`$${name}`)) {
					w.export_let_unused(binding.node, name);
				}
			}
		}

		analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements);
	}

	for (const node of analysis.module.ast.body) {
		if (node.type === 'ExportNamedDeclaration' && node.specifiers !== null && node.source == null) {
			for (const specifier of node.specifiers) {
				if (specifier.local.type !== 'Identifier') continue;

				const binding = analysis.module.scope.get(specifier.local.name);
				if (!binding) e.export_undefined(specifier, specifier.local.name);
			}
		}
	}

	if (analysis.event_directive_node && analysis.uses_event_attributes) {
		e.mixed_event_handler_syntaxes(
			analysis.event_directive_node,
			analysis.event_directive_node.name
		);
	}

	for (const [node, resolved] of analysis.snippet_renderers) {
		if (!resolved) {
			node.metadata.snippets = analysis.snippets;
		}

		for (const snippet of node.metadata.snippets) {
			snippet.metadata.sites.add(node);
		}
	}

	if (
		analysis.uses_render_tags &&
		(analysis.uses_slots || (!analysis.custom_element && analysis.slot_names.size > 0))
	) {
		const pos = analysis.slot_names.values().next().value ?? analysis.source.indexOf('$$slot');
		e.slot_snippet_conflict(pos);
	}

	if (analysis.css.ast) {
		analyze_css(analysis.css.ast, analysis);

		// mark nodes as scoped/unused/empty etc
		for (const node of analysis.elements) {
			prune(analysis.css.ast, node);
		}

		const { comment } = analysis.css.ast.content;
		const should_ignore_unused =
			comment &&
			extract_svelte_ignore(comment.start, comment.data, analysis.runes).includes(
				'css_unused_selector'
			);

		if (!should_ignore_unused) {
			warn_unused(analysis.css.ast);
		}
	}

	for (const node of analysis.elements) {
		if (node.metadata.scoped && is_custom_element_node(node)) {
			mark_subtree_dynamic(node.metadata.path);
		}

		let has_class = false;
		let has_style = false;
		let has_spread = false;
		let has_class_directive = false;
		let has_style_directive = false;

		for (const attribute of node.attributes) {
			// The spread method appends the hash to the end of the class attribute on its own
			if (attribute.type === 'SpreadAttribute') {
				has_spread = true;
				break;
			} else if (attribute.type === 'Attribute') {
				has_class ||= attribute.name.toLowerCase() === 'class';
				has_style ||= attribute.name.toLowerCase() === 'style';
			} else if (attribute.type === 'ClassDirective') {
				has_class_directive = true;
			} else if (attribute.type === 'StyleDirective') {
				has_style_directive = true;
			}
		}

		// We need an empty class to generate the set_class() or class="" correctly
		if (!has_spread && !has_class && (node.metadata.scoped || has_class_directive)) {
			node.attributes.push(
				create_attribute('class', -1, -1, [
					{
						type: 'Text',
						data: '',
						raw: '',
						start: -1,
						end: -1
					}
				])
			);
		}

		// We need an empty style to generate the set_style() correctly
		if (!has_spread && !has_style && has_style_directive) {
			node.attributes.push(
				create_attribute('style', -1, -1, [
					{
						type: 'Text',
						data: '',
						raw: '',
						start: -1,
						end: -1
					}
				])
			);
		}
	}

	// TODO
	// analysis.stylesheet.warn_on_unused_selectors(analysis);

	return analysis;
}
@param {AST.Root} root
@param {string} source
@param {ValidatedCompileOptions} options
@returns {ComponentAnalysis}