mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-30 19:43:47 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			370 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			370 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Code style and conventions
 | |
| 
 | |
| This page documents code style policies that every Zulip developer
 | |
| should understand. We aim for this document to be short and focused
 | |
| only on details that cannot be easily enforced another way (e.g.,
 | |
| through linters, automated tests, or subsystem design that makes classes
 | |
| of mistakes unlikely). This approach minimizes the cognitive
 | |
| load of ensuring a consistent coding style for both contributors and
 | |
| maintainers.
 | |
| 
 | |
| One can summarize Zulip's coding philosophy as a relentless focus on
 | |
| making the codebase easy to understand and difficult to make dangerous
 | |
| mistakes in (see the sections on [dangerous constructs](#dangerous-constructs-in-django)
 | |
| at the end of this page). The majority of work in any large software
 | |
| development project is understanding the existing code so one can debug
 | |
| or modify it, and investments in code readability usually end up paying
 | |
| for themselves when someone inevitably needs to debug or improve the code.
 | |
| 
 | |
| When there's something subtle or complex to explain or ensure in the
 | |
| implementation, we try hard to make it clear through a combination of
 | |
| clean and intuitive interfaces, well-named variables and functions,
 | |
| comments/docstrings, and commit messages (roughly in that order of
 | |
| priority -- if you can make something clear with a good interface,
 | |
| that's a lot better than writing a comment explaining how the bad
 | |
| interface works).
 | |
| 
 | |
| After an introduction to our lint tools and test suites, this document
 | |
| outlines some general
 | |
| [conventions and practices](#follow-zulip-conventions-and-practices)
 | |
| applicable to all languages used in the codebase, as well as specific
 | |
| guidance on [Python](#python-specific-conventions-and-practices) and
 | |
| [JavaScript and TypeScript](#javascript-and-typescript-conventions-and-practices).
 | |
| ([HTML and CSS](../subsystems/html-css.md) are outlined in their own
 | |
| documentation.)
 | |
| 
 | |
| At the end of the document, you can read about
 | |
| [dangerous constructs in Django](#dangerous-constructs-in-django) and
 | |
| [JavaScript and TypeScript](#dangerous-constructs-in-javascript-and-typescript)
 | |
| that you should absolutely avoid.
 | |
| 
 | |
| ## Be consistent with existing code
 | |
| 
 | |
| Look at the surrounding code, or a similar part of the project, and try
 | |
| to do the same thing. If you think the other code has actively bad
 | |
| style, fix it (in a separate commit).
 | |
| 
 | |
| When in doubt, ask in
 | |
| [#development help](https://chat.zulip.org/#narrow/channel/49-development-help).
 | |
| 
 | |
| ### Use the linters
 | |
| 
 | |
| You can run all of the linters at once:
 | |
| 
 | |
| ```bash
 | |
| $ ./tools/lint
 | |
| ```
 | |
| 
 | |
| Note that that takes a little time. `./tools/lint` runs many
 | |
| lint checks in parallel, including:
 | |
| 
 | |
| - JavaScript ([ESLint](https://eslint.org/),
 | |
|   [Prettier](https://prettier.io/))
 | |
| - Python ([mypy](http://mypy-lang.org/),
 | |
|   [Ruff](https://docs.astral.sh/ruff/))
 | |
| - templates
 | |
| - Puppet configuration
 | |
| - custom checks (e.g., trailing whitespace and spaces-not-tabs)
 | |
| 
 | |
| To speed things up, you can [pass specific files or directories
 | |
| to the linter](../testing/linters.md):
 | |
| 
 | |
| ```
 | |
| $ ./tools/lint web/src/compose.ts
 | |
| ```
 | |
| 
 | |
| If you'd like, you can also set up a local Git commit hook that
 | |
| will lint only your changed files each time you commit:
 | |
| 
 | |
| ```bash
 | |
| $ ./tools/setup-git-repo
 | |
| ```
 | |
| 
 | |
| ### Use tests to verify your logic
 | |
| 
 | |
| Clear, readable code is important for [tests](../testing/testing.md);
 | |
| familiarize yourself with our
 | |
| [testing frameworks](../testing/testing.md#major-test-suites) and
 | |
| [testing philosophy](../testing/philosophy.md) so that you can write
 | |
| clean, readable tests. In-test comments about anything subtle that is
 | |
| being verified are appreciated.
 | |
| 
 | |
| You can run all of the tests like this:
 | |
| 
 | |
| ```
 | |
| $ ./tools/test-all
 | |
| ```
 | |
| 
 | |
| But consult [our documentation on running tests](../testing/testing.md#running-tests),
 | |
| which covers more targeted approaches to commanding the test-runners.
 | |
| 
 | |
| ## Follow Zulip conventions and practices
 | |
| 
 | |
| What follows is language-neutral advice that is beyond the bounds of
 | |
| linters and automated tests.
 | |
| 
 | |
| ### Observe a reasonable line length
 | |
| 
 | |
| We have an absolute hard limit on line length only for some files, but
 | |
| we should still avoid extremely long lines. A general guideline is:
 | |
| refactor stuff to get it under 85 characters, unless that makes the
 | |
| code a lot uglier, in which case it's fine to go up to 120 or so.
 | |
| 
 | |
| ### Tag user-facing strings for translation
 | |
| 
 | |
| Remember to
 | |
| [tag all user-facing strings for translation](../translating/translating.md),
 | |
| whether the strings are in HTML templates or output by JavaScript/TypeScript
 | |
| that injects or modifies HTML (e.g., error messages).
 | |
| 
 | |
| ### Correctly prepare paths destined for state or log files
 | |
| 
 | |
| When writing out state or log files, always pass an absolute path
 | |
| through `zulip_path` (found in `zproject/computed_settings.py`), which
 | |
| will do the right thing in both development and production.
 | |
| 
 | |
| ### Never include secrets inline with code
 | |
| 
 | |
| Please don't put any passwords, secret access keys, etc. inline in the
 | |
| code. Instead, use the `get_secret` function or the `get_mandatory_secret`
 | |
| function in `zproject/config.py` to read secrets from `/etc/zulip/secrets.conf`.
 | |
| 
 | |
| ### Familiarize yourself with rules about third-party code
 | |
| 
 | |
| See [our docs on dependencies](../subsystems/dependencies.md) for discussion of
 | |
| rules about integrating third-party projects.
 | |
| 
 | |
| ## Python-specific conventions and practices
 | |
| 
 | |
| - Our Python code is formatted with
 | |
|   [Ruff](https://docs.astral.sh/ruff/). The [linter
 | |
|   tool](../testing/linters.md) enforces this by running Ruff in check
 | |
|   mode, or in write mode with
 | |
|   `tools/lint --only=ruff,ruff-format --fix`. You may find it helpful
 | |
|   to [integrate Ruff](https://docs.astral.sh/ruff/integrations/) with
 | |
|   your editor.
 | |
| - Don't put a shebang line on a Python file unless it's meaningful to
 | |
|   run it as a script. (Some libraries can also be run as scripts, e.g.,
 | |
|   to run a test suite.)
 | |
| - Scripts should be executed directly (`./script.py`), so that the
 | |
|   interpreter is implicitly found from the shebang line, rather than
 | |
|   explicitly overridden (`python script.py`).
 | |
| - Put all imports together at the top of the file, absent a compelling
 | |
|   reason to do otherwise.
 | |
| - Unpacking sequences doesn't require list brackets:
 | |
|   ```python
 | |
|   [x, y] = xs    # unnecessary
 | |
|   x, y = xs      # better
 | |
|   ```
 | |
| - For string formatting, use `x % (y,)` rather than `x % y`, to avoid
 | |
|   ambiguity if `y` happens to be a tuple.
 | |
| 
 | |
| ## JavaScript and TypeScript conventions and practices
 | |
| 
 | |
| Our JavaScript and TypeScript code is formatted with
 | |
| [Prettier](https://prettier.io/). You can ask Prettier to reformat
 | |
| all code via our [linter tool](../testing/linters.md) with
 | |
| `tools/lint --only=prettier --fix`. You can also [integrate it with your
 | |
| editor](https://prettier.io/docs/en/editors.html).
 | |
| 
 | |
| ### Build DOM elements in Handlebars
 | |
| 
 | |
| The best way to build complicated DOM elements is a Handlebars template
 | |
| like `web/templates/message_reactions.hbs`. For simpler things you can
 | |
| use jQuery DOM-building APIs like this:
 | |
| 
 | |
| ```js
 | |
| const $new_tr = $('<tr />').attr('id', object.id);
 | |
| ```
 | |
| 
 | |
| ### Attach behaviors to event listeners
 | |
| 
 | |
| Attach callback functions to events using jQuery code. For example:
 | |
| 
 | |
| ```js
 | |
| $("body").on("click", ".move_message_button", function (e) {
 | |
|   // message-moving UI logic
 | |
| }
 | |
| ```
 | |
| 
 | |
| That approach has multiple benefits:
 | |
| 
 | |
| - Potential huge performance gains by using delegated events where
 | |
|   possible
 | |
| - When calling a function from an `onclick` attribute, `this` is not
 | |
|   bound to the element like you might think
 | |
| - jQuery does event normalization
 | |
| 
 | |
| Do not use `onclick` attributes in the HTML.
 | |
| 
 | |
| ### Declare variables using `const` and `let`
 | |
| 
 | |
| Always declare JavaScript variables using `const` or `let` rather than
 | |
| `var`.
 | |
| 
 | |
| ### Manipulate objects and arrays with modern methods
 | |
| 
 | |
| For functions that operate on arrays or JavaScript objects, you should
 | |
| generally use modern
 | |
| [ECMAScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Language_Resources)
 | |
| primitives such as [`for … of`
 | |
| loops](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of),
 | |
| [`Array.prototype.{entries, every, filter, find, indexOf, map, some}`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array),
 | |
| [`Object.{assign, entries, keys, values}`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object),
 | |
| [spread
 | |
| syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax),
 | |
| and so on. Our Babel configuration automatically transpiles and
 | |
| polyfills these using [`core-js`](https://github.com/zloirock/core-js)
 | |
| when necessary. We used to use the
 | |
| [Underscore](https://underscorejs.org/) library, but that should be
 | |
| avoided in new code.
 | |
| 
 | |
| ## HTML and CSS
 | |
| 
 | |
| See the documentation on [HTML and CSS](../subsystems/html-css.md)
 | |
| for guidance on conventions in those language.
 | |
| 
 | |
| ## Dangerous constructs in Django
 | |
| 
 | |
| ### Avoid excessive database queries
 | |
| 
 | |
| Look out for Django code like this:
 | |
| 
 | |
| ```python
 | |
| bars = Bar.objects.filter(...)
 | |
| for bar in bars:
 | |
|     foo = bar.foo
 | |
|     # Make use of foo
 | |
| ```
 | |
| 
 | |
| ...because it equates to:
 | |
| 
 | |
| ```python
 | |
| bars = Bar.objects.filter(...)
 | |
| for bar in bars:
 | |
|     foo = Foo.objects.get(id=bar.foo.id)
 | |
|     # Make use of foo
 | |
| ```
 | |
| 
 | |
| ...which makes a database query for every `Bar`. While this may be fast
 | |
| locally in development, it may be quite slow in production! Instead,
 | |
| tell Django's [QuerySet
 | |
| API](https://docs.djangoproject.com/en/5.0/ref/models/querysets/) to
 | |
| _prefetch_ the data in the initial query:
 | |
| 
 | |
| ```python
 | |
| bars = Bar.objects.filter(...).select_related()
 | |
| for bar in bars:
 | |
|     foo = bar.foo  # This doesn't take another query, now!
 | |
|     # Make use of foo
 | |
| ```
 | |
| 
 | |
| If you can't rewrite it as a single query, that's a sign that something
 | |
| is wrong with the database schema. So don't defer this optimization when
 | |
| performing schema changes, or else you may later find that it's
 | |
| impossible.
 | |
| 
 | |
| ### Never do direct database queries (`UserProfile.objects.get()`, `Client.objects.get()`, etc.)
 | |
| 
 | |
| In our Django code, never do direct `UserProfile.objects.get(email=foo)`
 | |
| database queries. Instead always use `get_user_profile_by_{email,id}`.
 | |
| There are 3 reasons for this:
 | |
| 
 | |
| 1.  It's guaranteed to correctly do a case-inexact lookup
 | |
| 2.  It fetches the user object from remote cache, which is faster
 | |
| 3.  It always fetches a UserProfile object which has been queried
 | |
|     using `.select_related()` ([see above](#avoid-excessive-database-queries)!),
 | |
|     and thus will perform well when one later accesses related models
 | |
|     like the Realm.
 | |
| 
 | |
| Similarly we have `get_client` and `access_stream_by_id` /
 | |
| `access_stream_by_name` functions to fetch those commonly accessed
 | |
| objects via remote cache.
 | |
| 
 | |
| ### Don't use Django model objects as keys in sets/dicts
 | |
| 
 | |
| Don't use Django model objects as keys in sets/dictionaries -- you will
 | |
| get unexpected behavior when dealing with objects obtained from
 | |
| different database queries:
 | |
| 
 | |
| For example, the following will, surprisingly, fail:
 | |
| 
 | |
| ```python
 | |
| # Bad example -- will raise!
 | |
| obj: UserProfile = get_user_profile_by_id(17)
 | |
| some_objs = UserProfile.objects.get(id=17)
 | |
| assert obj in set([some_objs])
 | |
| ```
 | |
| 
 | |
| You should work with the IDs instead:
 | |
| 
 | |
| ```python
 | |
| obj: UserProfile = get_user_profile_by_id(17)
 | |
| some_objs = UserProfile.objects.get(id=17)
 | |
| assert obj.id in set([o.id for o in some_objs])
 | |
| ```
 | |
| 
 | |
| ### Don't call user_profile.save() without `update_fields`
 | |
| 
 | |
| You should always pass the `update_fields` keyword argument to `.save()`
 | |
| when modifying an existing Django model object. By default, `.save()` will
 | |
| overwrite every value in the column, which results in lots of race
 | |
| conditions where unrelated changes made by one thread can be
 | |
| accidentally overwritten by another thread that fetched its `UserProfile`
 | |
| object before the first thread wrote out its change.
 | |
| 
 | |
| ### Don't update important model objects with raw saves
 | |
| 
 | |
| In most cases, we already have a function in `zerver.actions` with
 | |
| a name like `do_activate_user` that will correctly handle lookups,
 | |
| caching, and notifying running browsers via the event system about your
 | |
| change. So please check whether such a function exists before writing
 | |
| new code to modify a model object, since your new code has a good chance
 | |
| of getting at least one of these things wrong.
 | |
| 
 | |
| ### Don't use naive datetime objects
 | |
| 
 | |
| Python allows datetime objects to not have an associated time zone, which can
 | |
| cause time-related bugs that are hard to catch with a test suite, or bugs
 | |
| that only show up during daylight saving time.
 | |
| 
 | |
| Good ways to make time-zone-aware datetimes are below. We import time zone
 | |
| libraries as `from datetime import datetime, timezone` and
 | |
| `from django.utils.timezone import now as timezone_now`.
 | |
| 
 | |
| Use:
 | |
| 
 | |
| - `timezone_now()` to get a datetime when Django is available, such as
 | |
|   in `zerver/`.
 | |
| - `datetime.now(tz=timezone.utc)` when Django is not available, such as
 | |
|   for bots and scripts.
 | |
| - `datetime.fromtimestamp(timestamp, tz=timezone.utc)` if creating a
 | |
|   datetime from a timestamp. This is also available as
 | |
|   `zerver.lib.timestamp.timestamp_to_datetime`.
 | |
| - `datetime.strptime(date_string, format).replace(tzinfo=timezone.utc)` if
 | |
|   creating a datetime from a formatted string that is in UTC.
 | |
| 
 | |
| Idioms that result in time-zone-naive datetimes, and should be avoided, are
 | |
| `datetime.now()` and `datetime.fromtimestamp(timestamp)` without a `tz`
 | |
| parameter, `datetime.utcnow()` and `datetime.utcfromtimestamp()`, and
 | |
| `datetime.strptime(date_string, format)` without replacing the `tzinfo` at
 | |
| the end.
 | |
| 
 | |
| Additional notes:
 | |
| 
 | |
| - Especially in scripts and puppet configuration where Django is not
 | |
|   available, using `time.time()` to get timestamps can be cleaner than
 | |
|   dealing with datetimes.
 | |
| - All datetimes on the backend should be in UTC, unless there is a good
 | |
|   reason to do otherwise.
 | |
| 
 | |
| ## Dangerous constructs in JavaScript and TypeScript
 | |
| 
 | |
| ### Do not use `for...in` statements to traverse arrays
 | |
| 
 | |
| That construct pulls in properties inherited from the prototype chain.
 | |
| Don't use it:
 | |
| [[1]](https://stackoverflow.com/questions/500504/javascript-for-in-with-arrays),
 | |
| [[2]](https://google.github.io/styleguide/javascriptguide.xml#for-in_loop),
 | |
| [[3]](https://www.jslint.com/help.html#forin)
 |